diff --git a/.eslintrc.js b/.eslintrc.js index 5a8fe9e979..972882ce73 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -15,12 +15,11 @@ module.exports = { 'no-prototype-builtins': 1, 'no-useless-constructor': 1, 'no-empty-function': 1, - '@typescript-eslint/member-ordering': 0, 'lines-between-class-members': 0, 'no-await-in-loop': 0, 'no-plusplus': 0, '@typescript-eslint/no-parameter-properties': 0, - '@typescript-eslint/no-unused-vars': 1, + 'no-restricted-exports': ['error'], 'no-multi-assign': 1, 'no-dupe-class-members': 1, 'react/no-deprecated': 1, @@ -35,10 +34,26 @@ module.exports = { '@typescript-eslint/indent': 0, 'import/no-cycle': 0, '@typescript-eslint/no-shadow': 0, - "@typescript-eslint/method-signature-style": 0, - "@typescript-eslint/consistent-type-assertions": 0, - "@typescript-eslint/no-useless-constructor": 0, + '@typescript-eslint/method-signature-style': 0, + '@typescript-eslint/consistent-type-assertions': 0, + '@typescript-eslint/no-useless-constructor': 0, '@typescript-eslint/dot-notation': 0, // for lint performance '@typescript-eslint/restrict-plus-operands': 0, // for lint performance - } + 'no-unexpected-multiline': 1, + 'no-multiple-empty-lines': ['error', { max: 1 }], + 'lines-around-comment': ['error', { + beforeBlockComment: true, + afterBlockComment: false, + afterLineComment: false, + allowBlockStart: true, + }], + 'comma-dangle': ['error', 'always-multiline'], + '@typescript-eslint/member-ordering': [ + 'error', + { default: ['signature', 'field', 'constructor', 'method'] } + ], + '@typescript-eslint/no-unused-vars': ['error'], + 'no-redeclare': 0, + '@typescript-eslint/no-redeclare': 1, + }, }; diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bc889c3079..670db7dfbd 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,13 +2,7 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence -* @leoyuan @JackLian +* @liujuping @1ncounter /modules/material-parser @akirakai -/modules/code-generator @Clarence-pan - -/packages/renderer-core/ @liujuping -/packages/react-renderer/ @liujuping -/packages/react-simulator-renderer/ @liujuping -/packages/rax-renderer/ @liujuping -/packages/rax-simulator-renderer/ @liujuping \ No newline at end of file +/modules/code-generator @qingniaotonghua diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 193cab77eb..c72471ee28 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,6 +1,6 @@ --- name: Bug report / 提交 bug -about: Create a report to help us improve / 提交一个好的 issue 帮助我们优化引擎,[引擎的 issue 说明](https://www.yuque.com/lce/doc/yvlxhs) +about: Create a report to help us improve / 提交一个好的 issue 帮助我们优化引擎,[引擎的 issue 说明](https://lowcode-engine.cn/site/community/issue) title: '' labels: '' assignees: '' diff --git a/.github/workflows/check base branch.yml b/.github/workflows/check base branch.yml new file mode 100644 index 0000000000..cef996c75a --- /dev/null +++ b/.github/workflows/check base branch.yml @@ -0,0 +1,33 @@ +name: Check Base Branch + +on: + pull_request: + types: [opened] + +jobs: + code-review: + name: Check + runs-on: ubuntu-latest + + steps: + # 判断用户是否有写仓库权限 + - name: 'Check User Permission' + uses: 'lannonbr/repo-permission-check-action@2.0.0' + with: + permission: 'write' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: 'Check base branch name is develop or not' + if: github.event.pull_request.base.ref != 'develop' # check the target branch if it's master + uses: actions-cool/issues-helper@v2 + with: + actions: 'create-comment' + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + 感谢你的 PR,根据引擎的 [研发协作流程](https://lowcode-engine.cn/site/docs/participate/flow),请将目标合入分支设置为 **develop**。 + + Thanks in advance, according to the [Contribution Guideline](https://lowcode-engine.cn/site/docs/participate/flow), please set the base branch to **develop**. + + @${{ github.event.pull_request.user.login }} \ No newline at end of file diff --git a/.github/workflows/cov packages.yml b/.github/workflows/cov packages.yml index 228e1cf989..7f92e1009c 100644 --- a/.github/workflows/cov packages.yml +++ b/.github/workflows/cov packages.yml @@ -71,4 +71,26 @@ jobs: working-directory: packages/react-simulator-renderer test-script: npm test -- --jest-ci --jest-json --jest-coverage --jest-testLocationInResults --jest-outputFile=report.json package-manager: yarn + annotations: none + + cov-utils: + runs-on: ubuntu-latest + # skip fork's PR, otherwise it fails while making a comment + if: ${{ github.event.pull_request.head.repo.full_name == 'alibaba/lowcode-engine' }} + 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 + + - uses: ArtiomTr/jest-coverage-report-action@v2 + with: + working-directory: packages/utils + test-script: npm test -- --jest-ci --jest-json --jest-coverage --jest-testLocationInResults --jest-outputFile=report.json + package-manager: yarn annotations: none \ No newline at end of file diff --git a/.github/workflows/help wanted.yml b/.github/workflows/help wanted.yml index 94927ad28f..619d08b936 100644 --- a/.github/workflows/help wanted.yml +++ b/.github/workflows/help wanted.yml @@ -1,4 +1,4 @@ -name: Issue Reply +name: Help Wanted on: issues: diff --git a/.github/workflows/insufficient information.yml b/.github/workflows/insufficient information.yml index 2b699860d6..c49e133f16 100644 --- a/.github/workflows/insufficient information.yml +++ b/.github/workflows/insufficient information.yml @@ -1,4 +1,4 @@ -name: Issue Reply +name: Insufficient Info on: issues: @@ -16,4 +16,4 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} issue-number: ${{ github.event.issue.number }} body: | - 你好 @${{ github.event.issue.user.login }},由于缺乏必要的信息(如 bug 重现步骤、引擎版本信息 等),无法定位问题,请按照 [issue bug 模板](https://github.com/alibaba/lowcode-engine/blob/main/.github/ISSUE_TEMPLATE/bug-report.md) 补全信息,也可以通过阅读[引擎的 issue 说明](https://www.yuque.com/lce/doc/yvlxhs) 了解什么类型的 issue 可以获得更好、更快的支持。 + 你好 @${{ github.event.issue.user.login }},由于缺乏必要的信息(如 bug 重现步骤、引擎版本信息 等),无法定位问题,请按照 [issue bug 模板](https://github.com/alibaba/lowcode-engine/blob/main/.github/ISSUE_TEMPLATE/bug-report.md) 补全信息,也可以通过阅读 [引擎的 issue 说明](https://lowcode-engine.cn/site/community/issue) 了解什么类型的 issue 可以获得更好、更快的支持。 diff --git a/.github/workflows/pr comment by chatgpt.yml b/.github/workflows/pr comment by chatgpt.yml new file mode 100644 index 0000000000..52585c4778 --- /dev/null +++ b/.github/workflows/pr comment by chatgpt.yml @@ -0,0 +1,23 @@ +name: Pull Request Review By ChatGPT + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + code-review: + name: Code Review + runs-on: ubuntu-latest + + steps: + # 判断用户是否有写仓库权限 + - name: 'Check User Permission' + uses: 'lannonbr/repo-permission-check-action@2.0.0' + with: + permission: 'write' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - uses: opensumi/actions/.github/actions/code-review@main + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/pre build.yml b/.github/workflows/pre build.yml new file mode 100644 index 0000000000..e6f7d6479d --- /dev/null +++ b/.github/workflows/pre build.yml @@ -0,0 +1,34 @@ +name: Pre Build + +on: + push: + paths: + - 'packages/**' + - '!packages/**.md' + pull_request: + paths: + - 'packages/**' + - '!packages/**.md' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Install dependencies and setup + run: npm install && npm run setup + + - name: Build + run: npm run build + + - name: Check build status + run: | + if [ $? -eq 0 ]; then + echo "Build succeeded!" + else + echo "Build failed!" + exit 1 + fi diff --git a/.github/workflows/publish docs.yml b/.github/workflows/publish docs.yml new file mode 100644 index 0000000000..139b70239f --- /dev/null +++ b/.github/workflows/publish docs.yml @@ -0,0 +1,53 @@ +name: Update and Publish Docs + +on: + push: + branches: + - develop + paths: + - 'docs/docs/**' + workflow_dispatch: + +jobs: + publish-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + ref: 'develop' + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - run: cd docs && npm install + - run: | + cd docs + npm version patch + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add package.json + git commit -m "chore(docs): publish documentation" + git push + - run: cd docs && npm run build && npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Get version + id: get_version + run: echo "version=$(node -p "require('./docs/package.json').version")" >> $GITHUB_OUTPUT + + comment-pr: + needs: publish-docs + runs-on: ubuntu-latest + steps: + - name: Comment on PR + if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true + uses: actions/github-script@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + github.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: '🚀 New version has been released: ' + '${{ needs.publish-docs.outputs.version }}' + }) diff --git a/.github/workflows/publish engine beta.yml b/.github/workflows/publish engine beta.yml new file mode 100644 index 0000000000..ed4c374756 --- /dev/null +++ b/.github/workflows/publish engine beta.yml @@ -0,0 +1,30 @@ +name: Publish Engine Beta + +on: + push: + branches: + - 'release/[0-9]+.[0-9]+.[0-9]+-beta' + paths: + - 'packages/**' + +jobs: + publish-engine: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '14' + registry-url: 'https://registry.npmjs.org' + - run: npm install && npm run setup + - run: | + npm run build + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + - run: npm run pub:prerelease + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Get version + id: get_version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT diff --git a/.github/workflows/publish engine.yml b/.github/workflows/publish engine.yml new file mode 100644 index 0000000000..ddbefcde55 --- /dev/null +++ b/.github/workflows/publish engine.yml @@ -0,0 +1,33 @@ +name: Publish Engine + +on: + workflow_dispatch: + inputs: + publishCommand: + description: 'publish command' + required: true + +jobs: + publish-engine: + runs-on: ubuntu-latest + if: >- + contains(github.ref, 'refs/heads/release/') && + (github.actor == '1ncounter' || github.actor == 'liujuping') + steps: + - uses: actions/checkout@v2 + - name: Setup Node.js + uses: actions/setup-node@v2 + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - run: npm install && npm run setup + - run: | + npm run build + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + - run: npm run ${{ github.event.inputs.publishCommand }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + - name: Get version + id: get_version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT diff --git a/.github/workflows/test modules.yml b/.github/workflows/test modules.yml index 9410626e88..b2464cc40c 100644 --- a/.github/workflows/test modules.yml +++ b/.github/workflows/test modules.yml @@ -1,4 +1,4 @@ -name: lint & test +name: Lint & Test (Mods) on: push: diff --git a/.github/workflows/test packages.yml b/.github/workflows/test packages.yml index cc05742d54..45fa665465 100644 --- a/.github/workflows/test packages.yml +++ b/.github/workflows/test packages.yml @@ -1,4 +1,4 @@ -name: lint & test +name: Lint & Test (Pkgs) on: push: @@ -41,4 +41,100 @@ jobs: run: npm i && npm run setup:skip-build - name: test - run: cd packages/designer && npm test \ No newline at end of file + run: cd packages/designer && npm test + + test-editor-skeleton: + 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-skeleton && npm test + + test-renderer-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/renderer-core && npm test + + test-react-simulator-renderer: + 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/react-simulator-renderer && npm test + + test-utils: + 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/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/.gitignore b/.gitignore index dc6504d08b..6a19ae3e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ packages/*/output/ packages/demo/ package-lock.json yarn.lock +pnpm-lock.yaml deploy-space/packages deploy-space/.env @@ -107,3 +108,5 @@ typings/ # codealike codealike.json .node + +.must.config.js \ No newline at end of file diff --git a/CONTRIBUTOR.md b/CONTRIBUTOR.md index 89757bac92..11d50baade 100644 --- a/CONTRIBUTOR.md +++ b/CONTRIBUTOR.md @@ -24,5 +24,6 @@ - [Ychangqing](https://github.com/Ychangqing) - [yize](https://github.com/yize) - [youluna](https://github.com/youluna) +- [ibreathebsb](https://github.com/ibreathebsb) 如果您贡献过低代码引擎,但是没有看到您的名字,为我们的疏忽感到抱歉。欢迎您通过 PR 补充上自己的名字。 diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000000..a089167a78 --- /dev/null +++ b/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + ['@babel/plugin-proposal-decorators', { legacy: true }], + [require.resolve('@babel/plugin-proposal-class-properties'), { loose: true }], + ], +}; \ No newline at end of file diff --git a/deploy-space/static/index.html b/deploy-space/static/index.html index 3b4cbddc29..e7ff4ba730 100644 --- a/deploy-space/static/index.html +++ b/deploy-space/static/index.html @@ -21,7 +21,7 @@ + + + + + + + + + + + + + + + +``` +> 注:如果 unpkg 的服务比较缓慢,您可以使用官方 CDN 来获得确定版本的低代码引擎,如对于引擎的 1.0.18 版本,可用以下官方 CDN 替代 +> - [https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/1.0.18/dist/js/engine-core.js](https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/1.0.18/dist/js/engine-core.js) + + +### 配置打包 + +因为这些资源已经通过 UMD 方式引入,所以在 webpack 等构建工具中需要配置它们为 external,不再重复打包: + +```javascript +{ + "externals": { + "react": "var window.React", + "react-dom": "var window.ReactDOM", + "prop-types": "var window.PropTypes", + "@alifd/next": "var window.Next", + "@alilc/lowcode-engine": "var window.AliLowCodeEngine", + "@alilc/lowcode-engine-ext": "var window.AliLowCodeEngineExt", + "moment": "var window.moment", + "lodash": "var window._" + } +} +``` + +### 初始化低代码编辑器 + +正确引入后,我们可以直接通过 window 上的变量进行引用,如 `window.AliLowCodeEngine.init`。您可以直接通过此方式初始化低代码引擎: + +```javascript +// 确保在执行此命令前,在 中已有一个 id 为 lce-container 的
+window.AliLowCodeEngine.init(document.getElementById('lce-container'), { + enableCondition: true, + enableCanvasLock: true, +}); +``` + +如果您的项目中使用了 TypeScript,您可以通过如下 devDependencies 引入相关包,并获得对应的类型推断。 +```javascript +// package.json +{ + "devDependencies": { + "@alilc/lowcode-engine": "^1.0.0" + } +} +``` +```javascript +// src/index.tsx +import { init } from '@alilc/lowcode-engine'; + +init(document.getElementById('lce-container'), { + enableCondition: true, + enableCanvasLock: true, +}); +``` + +init 的功能包括但不限于: + +1. 传递 options 并设置 config 对象; +2. 传递 preference 并设置 plugins 入参; +3. 初始化 Workbench; + +> 本节中的低代码编辑器例子可以在 demo 中找到:[https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/index.ts](https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/index.ts) + +## 配置低代码编辑器 +详见[低代码扩展简述](/site/docs/guide/expand/editor/summary)章节。 diff --git a/docs/docs/guide/create/useRenderer.md b/docs/docs/guide/create/useRenderer.md new file mode 100644 index 0000000000..a9fc79909e --- /dev/null +++ b/docs/docs/guide/create/useRenderer.md @@ -0,0 +1,106 @@ +--- +title: 接入运行时 +sidebar_position: 1 +--- + +低代码引擎的编辑器将产出两份数据: + +- 资产包数据 assets:包含物料名称、包名及其获取方式,对应协议中的[《低代码引擎资产包协议规范》](/site/docs/specs/assets-spec) +- 页面数据 schema:包含页面结构信息、生命周期和代码信息,对应协议中的[《低代码引擎搭建协议规范》](/site/docs/specs/lowcode-spec) + +经过上述两份数据,可以直接交由渲染模块或者出码模块来运行,二者的区别在于: + +- 渲染模块:使用资产包数据、页面数据和低代码运行时,并且允许维护者在低代码编辑器中用 `低代码(LowCode)`的方式继续维护; +- 出码模块:不依赖低代码运行时和页面数据,直接生成可直接运行的代码,并且允许维护者用 `源码(ProCode)` 的方式继续维护,但无法再利用低代码编辑器; + +> 渲染和出码的详细阐述可参考此文:[低代码技术在研发团队的应用模式探讨](https://mp.weixin.qq.com/s/Ynk_wjJbmNw7fEG6UtGZbQ) + +## 渲染模块 + +[在 Demo 中](https://lowcode-engine.cn/demo/demo-general/index.html),右上角有渲染模块的示例使用方式: +![Mar-13-2022 16-52-49.gif](https://img.alicdn.com/imgextra/i2/O1CN01PRsEl61o7Zct5fJML_!!6000000005178-1-tps-1534-514.gif) + +基于官方提供的渲染模块 [@alifd/lowcode-react-renderer](https://github.com/alibaba/lowcode-engine/tree/main/packages/react-renderer),你可以在 React 上下文渲染低代码编辑器产出的页面。 + +### 构造渲染模块所需数据 + +渲染模块所需要的数据需要通过编辑器产出的数据进行一定的转换,规则如下: + +- schema:从编辑器产出的 projectSchema 中拿到 componentsTree 中的首项,即 `projectSchema.componentsTree[0]`; +- components:需要根据编辑器产出的资产包 assets 中,根据页面 projectSchema 中声明依赖的 componentsMap,来加载所有依赖的资产包,最后获取资产包的实例并生成物料 - 资产包的键值对 components。 + +这个过程可以参考 demo 项目中的 `src/preview.tsx`: + +```typescript +async function getSchemaAndComponents() { + const packages = JSON.parse(window.localStorage.getItem('packages') || ''); + const projectSchema = JSON.parse(window.localStorage.getItem('projectSchema') || ''); + const { componentsMap: componentsMapArray, componentsTree } = projectSchema; + const componentsMap: any = {}; + componentsMapArray.forEach((component: any) => { + componentsMap[component.componentName] = component; + }); + const schema = componentsTree[0]; + + const libraryMap = {}; + const libraryAsset = []; + packages.forEach(({ package: _package, library, urls, renderUrls }) => { + libraryMap[_package] = library; + if (renderUrls) { + libraryAsset.push(renderUrls); + } else if (urls) { + libraryAsset.push(urls); + } + }); + + const vendors = [assetBundle(libraryAsset, AssetLevel.Library)]; + + const assetLoader = new AssetLoader(); + await assetLoader.load(libraryAsset); + const components = await injectComponents(buildComponents(libraryMap, componentsMap)); + + return { + schema, + components, + }; +} +``` + +### 进行渲染 + +拿到 schema 和 components 以后,您可以借由资产包数据和页面数据来完成页面的渲染: +```tsx +import React from 'react'; +import ReactRenderer from '@alilc/lowcode-react-renderer'; + +const SamplePreview = () => { + return ( + + ); +} +``` + +> 注 1:您可以注意到,此处是依赖了 React 进行渲染的,对于 Vue 形态的渲染或编辑器支持,详见[对应公告](https://github.com/alibaba/lowcode-engine/issues/236)。 +> +> 注 2:本节示例可在 Demo 代码里找到更完整的版本:[https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/preview.tsx](https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/preview.tsx) + + +## 出码模块 + +[在 Demo 中](https://lowcode-engine.cn/demo/demo-general/index.html),右上角有出码模块的示例使用方式: + +![Mar-13-2022 16-55-56.gif](https://img.alicdn.com/imgextra/i3/O1CN017CVeka27p3vwrGI1D_!!6000000007845-1-tps-1536-514.gif) + +> 本节示例可在出码插件里找到:[https://github.com/alibaba/lowcode-code-generator-demo](https://github.com/alibaba/lowcode-code-generator-demo) + + +## 低代码的生产和消费流程总览 + +经过“接入编辑器” - “接入运行时”这两节的介绍,我们已经可以了解到低代码所构建的生产和消费流程了,梳理如下图: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01yiFiUc1rT32o9HpnW_!!6000000005631-2-tps-3206-1786.png) + +如上述流程所示,您一般需要一个后端项目来保存页面数据信息,如果资产包信息是动态的,也需要保存资产包信息。 diff --git a/docs/docs/guide/design/_category_.json b/docs/docs/guide/design/_category_.json new file mode 100644 index 0000000000..1868732be3 --- /dev/null +++ b/docs/docs/guide/design/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "引擎设计原理", + "position": 3, + "collapsed": false, + "collapsible": true +} diff --git a/docs/docs/guide/design/datasourceEngine.md b/docs/docs/guide/design/datasourceEngine.md new file mode 100644 index 0000000000..33c7adb082 --- /dev/null +++ b/docs/docs/guide/design/datasourceEngine.md @@ -0,0 +1,152 @@ +--- +title: 数据源引擎设计 +sidebar_position: 7 +--- +## 核心原理 + +考虑之后的扩展性和兼容性,核心分为了 2 类包,一个是 **datasource-engine** ,另一个是 **datasource-engine-x-handler** ,x 的意思其实是对应数据源的 type,比如说 **datasource-engine-mtop-handler**,也就是说我们会将真正的请求工具放在 handler 里面去处理,engine 在使用的时候由使用方自身来决定需要注册哪些 handler,这样的目的有 2 个,一个是如果将所有的 handler 都放到一个包,对于端上来说这个包过大,有一些浪费资源和损耗性能的问题,另一个是如果有新的类型的数据源出现,只需要按照既定的格式去新增一个对应的 handler 处理器即可,达到了高扩展性的目的; + +![](https://img.alicdn.com/imgextra/i3/O1CN011ep9No2ACzrgzgtk0_!!6000000008168-2-tps-720-370.png) + +### DataSourceEngine + +- engine:engine 主要分 2 类,一类是面向 render 引擎的,可以从 engine/interpret 引入,一类是面向出码或者说直接单纯使用数据源引擎的场景,可以从 engine/runtime 引入,代码如下 + +```typescript +import { createInterpret, createRuntime } from '@alilc/lowcode-datasource-engine'; +``` + +create 方法定义如下 + +```typescript +interface IDataSourceEngineFactory { + create(dataSource: DataSource, context: Omit, extraConfig?: { + requestHandlersMap: RequestHandlersMap; + [key: string]: any; + }): IDataSourceEngine; +} +``` + +create 接收三个参数,第一个是 DataSource,对于运行时渲染和出码来说,DataSource 的定义分别如下: + +```typescript +/** + * 数据源对象--运行时渲染 + */ +export interface DataSource { + list: DataSourceConfig[]; + dataHandler?: JSFunction; +} + +/** + * 数据源对象 + */ +export interface DataSourceConfig { + id: string; + isInit: boolean | JSExpression; + type: string; + requestHandler?: JSFunction; + dataHandler?: JSFunction; + options?: { + uri: string | JSExpression; + params?: JSONObject | JSExpression; + method?: string | JSExpression; + isCors?: boolean | JSExpression; + timeout?: number | JSExpression; + headers?: JSONObject | JSExpression; + [option: string]: CompositeValue; + }; + [otherKey: string]: CompositeValue; +} +``` + +但是对于出码来说,create 和 DataSource 定义如下: + +```typescript +export interface IRuntimeDataSourceEngineFactory { + create(dataSource: RuntimeDataSource, context: Omit, extraConfig?: { + requestHandlersMap: RequestHandlersMap; + [key: string]: any; + }): IDataSourceEngine; +} + +export interface RuntimeOptionsConfig { + uri: string; + params?: Record; + method?: string; + isCors?: boolean; + timeout?: number; + headers?: Record; + shouldFetch?: () => boolean; + [option: string]: unknown; +} +export declare type RuntimeOptions = () => RuntimeOptionsConfig; // 考虑需要动态获取值的情况,这里在运行时会真正的转为一个 function + +export interface RuntimeDataSourceConfig { + id: string; + isInit: boolean; + type: string; + requestHandler?: () => {}; + dataHandler: (data: unknown, err?: Error) => {}; + options?: RuntimeOptions; + [otherKey: string]: unknown; +} + +/** + * 数据源对象 + */ +export interface RuntimeDataSource { + list: RuntimeDataSourceConfig[]; + dataHandler?: (dataMap: DataSourceMap) => void; +} +``` + +2 者的区别还是比较明显的,一个是带 js 表达式一类的字符串,另一个是真正转为直接可以运行的 js 代码,对于出码来说,转为可执行的 js 代码的过程是出码自身负责的,对于渲染引擎来说,它只能接受到初始的 schema json 所以需要数据源引擎来做转化 + +- context:数据源引擎内部有一些使用了 this 的表达式,这些表达式需要求值的时候依赖上下文,因此需要将当前的上下文丢给数据源引擎,另外在 handler 里面去赋值的时候,也会用到诸如 setState 这种上下文里面的 api,当然,这个是可选的,我们后面再说。 + +```typescript +/** + * 运行时上下文--暂时是参考 react,当然可以自己构建,完全没问题 + */ +export interface IRuntimeContext> { + /** 当前容器的状态 */ + readonly state: TState; + /** 设置状态 (浅合并) */ + setState(state: Partial): void; + /** 自定义的方法 */ + [customMethod: string]: any; + /** 数据源,key 是数据源的 ID */ + dataSourceMap: Record; + /** 重新加载所有的数据源 */ + reloadDataSource(): Promise; + /** 页面容器 */ + readonly page: IRuntimeContext & { + readonly props: Record; + }; + /** 低代码业务组件容器 */ + readonly component: IRuntimeContext & { + readonly props: Record; + }; +} +``` + +- extraConfig:这个字段是为了留着扩展用的,除了一个必填的字段 **requestHandlersMap** + +```typescript +export declare type RequestHandler = (ds: RuntimeDataSourceConfig, context: IRuntimeContext) => Promise>; +export declare type RequestHandlersMap = Record; +``` + +RequestHandlersMap 是一个把数据源以及对应的数据源 handler 关联起来的桥梁,它的 key 对应的是数据源 DataSourceConfig 的 type,比如 mtop/http/jsonp ... ,每个类型的数据源在真正使用的时候会调用对应的 type-handler,并将当前的参数和上下文带给对应的 handler。 + +create 调用结束后,可以获取到一个 DataSourceEngine 实例 + +```typescript +export interface IDataSourceEngine { + /** 数据源,key 是数据源的 ID */ + dataSourceMap: Record; + /** 重新加载所有的数据源 */ + reloadDataSource(): Promise; +} +``` diff --git a/docs/docs/guide/design/editor.md b/docs/docs/guide/design/editor.md new file mode 100644 index 0000000000..0614d9c332 --- /dev/null +++ b/docs/docs/guide/design/editor.md @@ -0,0 +1,368 @@ +--- +title: 编排模块设计 +sidebar_position: 3 +--- +本篇重点介绍如何从零开始设计编排模块,设计思路是什么?思考编排的本质是什么?围绕着本质,如何设计并实现对应的功能模块。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01fGzyI41bqpl6AavNp_!!6000000003517-2-tps-1920-1080.png) + +## 编排是什么 + +所谓编排,即将设计器中的所有物料,进行布局设置、组件设置、交互设置(JS 编写/逻辑编排)后,形成符合业务诉求的 schema 描述。 +## 编排的本质 + +首先,思考编排的本质是什么? + +编排的本质是生产符合《阿里巴巴中后台前端搭建协议规范》的数据**,**在这个场景里,协议是通过 JSON 来承载的。如: + +```json +{ + "componentName": "Page", + "props": { + "layout": "wide" + }, + "children": [ + { + "componentName": "Button", + "props": { + "size": "large" + } + } + ] +} +``` + +可是在真实场景,节点数可能有成百上千,每个节点都具有新增、删除、修改、移动、插入子节点等操作,同时还有若干约束,JSON 结构操作起来不是很便利,于是我们仿 DOM 设计了 **节点模型 & 属性模型,**用更具可编程性的方式来编排,这是**编排系统的基石**。 + +其次,每次编排动作后(CRUD),都需要实时的渲染出视图。广义的视图应该包括各种平台上的展现,浏览器、Rax、小程序、Flutter 等等,所以使用何种渲染器去渲染 JSON 结构应该可以由用户去扩展,我们定义一种机制去衔接设计态和渲染态。 + +至此,我们已经完成了**编排模块最基础的功能**,接下来,就是完善细节,逐步丰满功能。比如: +1. 编排面板的整体功能区划分设计; +2. 节点属性设计;节点删除、移动等操作设计;容器节点设计; +3. 节点拖拽功能、拖拽定位设计和实现; +4. 节点在画布上的辅助功能,比如 hover、选中、选中时的操作项、resize、拖拽占位符等; +5. 设计态和渲染态的坐标系转换,滚动监听等; +6. 快捷键机制; +7. 历史功能,撤销和重做; +8. 结构化的插件扩展机制; +9. 原地编辑功能; + +有非常多模块,但只要记住一点,这些功能的目的都是辅助用户在画布上有更好的编排体验、扩展能力而逐个增加设计的。 + +## 编排功能模块 +### 模型设计 + +编排实际上操作 schema,但是实际代码运行的过程中,我们将 schema 分成了很多层,每一层有各自的职责,他们所负责的功能是明确清晰的。这就是低代码引擎中的模型设计。 + +我们通过将 schema 和常用的操作等结合起来,最终将低代码引擎的模型分为节点模型、属性模型、文档模型和项目模型。 + +#### 项目模型(`Project`) + +项目模型提供项目管理能力。通常一个引擎启动会默认创建一个 `Project` 实例,有且只有一个。项目模型实例下可以持有多个文档模型的实例,而当前处于设计器设计状态的文档模型,我们将其添加 active 标识,也将其称为 `currentDocument`,可以通过 `project.currentDocument` 获得。 + +一个 `Project` 包含若干个 `DocumentModel` 实例,即项目模型和文档模型的关系是 1 对 n,如下图所示: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01G28BKC1RvHRvhhiDf_!!6000000002173-2-tps-1226-1648.png) + +#### 文档模型(`DocumentModel`) + +文档模型提供文档管理的能力,每一个页面即一个文档流,对应一个文档模型。文档模型包含了一组 Node 组成的一颗树,类似于 DOM。我们可以通过文档模型来操作 `Node` 树,来达到管理文档模型的能力。每一个文档模型对应多个 `Node`,但是根 `Node` 只有一个,即 `rootNode` 和 `nodes`。 + +文档模型可以通过 `Node` 树,通过 `doc.schema` 来导出文档的 `schema`,并使用其进行渲染。 + +他们的关系如下图: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01NYVhN61nab6hsw5ZK_!!6000000005106-2-tps-960-1490.png) + +#### 节点模型(`Node`) + +我们先看一下一个 `Node` 在 `schema` 中对应的示例: + +```javascript +{ + componentName: 'Text', + id: 'node_k1ow3cbf', + props: { + showTitle: false, + behavior: 'NORMAL', + content: { + use: 'zh_CN', + en_US: 'Title', + zh_CN: '个人信息', + type: 'i18n', + }, + fieldId: 'text_k1ow3h1j', + maxLine: 0, + }, + condition: true, +} +``` + +上面的示例是一个 `Text` 的 `Node` 节点,而我们的 `Node` 节点模型就是负责这一层级的 `Schema` 管理。它的功能聚焦于单层级的 schema 相关操作。我们可以看一下节点模型的一些方法,了解其功能。 + +```typescript +declare class Node { + // Props + props: Props; + get propsData(): PropsMap | PropsList | null; + getProp(path: string, stash?: boolean): Prop | null; + getPropValue(path: string): any; + setPropValue(path: string, value: any): void; + clearPropValue(path: string): void; + mergeProps(props: PropsMap): void; + setProps(props?: PropsMap | PropsList | Props | null): void; + + // Node + get parent(): ParentalNode | null; + get children(): NodeChildren | null; + get nextSibling(): Node | null; + get prevSibling(): Node | null; + remove(useMutator?: boolean, purge?: boolean): void; + select(): void; + hover(flag?: boolean): void; + replaceChild(node: Node, data: any): Node; + mergeChildren(remover: () => any, adder: (children: Node[]) => NodeData[] | null, sorter: () => any): void; + removeChild(node: Node): void; + insert(node: Node, ref?: Node, useMutator?: boolean): void; + insertBefore(node: any, ref?: Node, useMutator?: boolean): void; + insertAfter(node: any, ref?: Node, useMutator?: boolean): void; + + // Schema + get schema(): Schema; + set schema(data: Schema); + export(stage?: TransformStage): Schema; + replaceWith(schema: Schema, migrate?: boolean): any; +} +``` + +这里没有展示全部的方法,但是我们可以发现,`Node` 节点模型核心功能点有三个: + +1. `Props` 管理:通过 `Props` 实例管理所有的 `Prop`,包括新增、设置、删除等 `Prop` 相关操作。 +2. `Node` 管理:管理 `Node` 树的关系,修改当前 `Node` 节点或者 `Node` 子节点等。 +3. `Schema` 管理:可以通过 `Node` 获取当前层级的 `Schema` 描述协议内容,并且也可以修改它。 + +通过 `Node` 这一层级,对 `Props`、`Node` 树和 `Schema` 的管理粒度控制到最低,这样扩展性也就更强。 + +#### 属性模型(Prop) + +一个 `Props` 对应多个 `Prop`,每一个 `Prop` 对应 schema 的 `props` 下的一个字段。 + +`Props` 管理的是 `Node` 节点模型中的 `props` 字段下的内容。而 `Prop` 管理的是 `props` 下的每一个 `key` 的内容,例如下面的示例中,一个 `Props` 管理至少 6 个 `Prop`,而其中一个 `Prop` 管理的是 `showTitle` 的结果。 + +```javascript +{ + props: { + showTitle: false, + behavior: 'NORMAL', + content: { + use: 'zh_CN', + en_US: 'Title', + zh_CN: '个人信息', + type: 'i18n', + }, + fieldId: 'text_k1ow3h1j', + maxLine: 0, + }, +} +``` +#### 组件描述模型(ComponentMeta) + +编排已经等价于直接操作节点 & 属性了,而一个节点和一组对应的属性相当于一个真实的组件,而真实的组件一定是有约束的,比如组件名、组件类型、支持哪些属性以及属性类型、组件能否拖动、支持哪些扩展操作、组件是否是容器型组件、A 组件中能否放入 B 组件等等。 + +于是,我们设计了一份协议专门负责组件描述,即《中后台搭建组件描述协议》,而编排模块中也有负责解析和使用符合描述协议规范的模块。 + +每一个组件对应一个 `ComponentMeta` 的实例,其属性和方法就是描述协议中的所有字段,所有 `ComponentMeta` 都由设计器器的 `designer` 模块进行创建和管理,其他模块通过 `designer` 来获取指定的 `ComponentMeta` 实例,尤其是每个 `Node` 实例上都会挂载对应的 `ComponentMeta` 实例。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01NSh0LI1b150RUzOUc_!!6000000003404-2-tps-998-756.png) + +组件描述模型是后续编排辅助的基础,包括设置面板、拖拽定位机制等。 +#### 项目、文档、节点和属性模型关系 + +整体来看,一个 Project 包含若干个 DocumentModel 实例,每个 DocumentModel 包含一组 Node 构成一颗树(类似 DOM 树),每个 Node 通过 Props 实例管理所有 Prop。整体的关系图如下。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01mufxpY1qCGvDTSdw9_!!6000000005459-2-tps-1694-1356.png) + +节点 & 属性模型是引擎基石,几乎贯穿所有模块,相信从上面的类图已经能看出几个基础类的职责以及依赖关系。 + +节点 & 属性模型等价于 JSON 数据结构,而编排的本质是产出 JSON 数据结构,现在可以重新表述为编排的本质是操作节点 & 属性模型了。 + +```typescript +// 一段编排的示例代码 +rootNode.insertAfter({ componentName: 'Button', props: { size: 'medium' } }); +rootNode.insertAfter({ componentName: 'Button', props: { size: 'medium' } }); +rootNode.children.get(1).getProp('size').setValue('large'); +rootNode.children.get(2).remove(); +rootNode.export(); +// => 产出 schema +``` + +### 画布渲染 + +画布渲染使用了设计态与渲染态的双层架构。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01cZ6Q32260qtiDofwi_!!6000000007600-2-tps-1416-710.png) + +如上图,设计器和渲染器其实处在不同的 Frame 下,渲染器以单独的 `iframe` 嵌入。这样做的好处,一是为了给渲染器一个更纯净的运行环境,更贴近生产环境,二是扩展性考虑,让用户基于接口约束自定义自己的渲染器。 + +#### xxx-renderer + +xxx-renderer 是一个纯 renderer,即一个渲染器,通过给定输入 schema、依赖组件和配置参数之后完成渲染。 + +#### xxx-simulator-renderer + +xxx-simulator-renderer 通过和 host 进行通信来和设计器打交道,提供了 `DocumentModel` 获取 schema 和组件。将其传入 xxx-renderer 来完成渲染。 + +另外其提供了一些必要的接口,来帮助设计器完成交互,比如点击渲染画布任意一个位置,需要能计算出点击的组件实例,继而找到设计器对应的 Node 实例,以及组件实例的位置/尺寸信息,让设计器完成辅助 UI 的绘制,如节点选中。 + +#### react-simulator-renderer + +以官方提供的 react-simulator-renderer 为例,我们看一下点击一个 DOM 节点后编排模块是如何处理的。 + +首先在初始化的时候,renderer 渲染的时候会给每一个元素添加 ref,通过 ref 机制在组件创建时将其存储起来。在存储的时候我们给实例添加 `Symbol('_LCNodeId')` 的属性。 + +当点击之后,会去根据 `__reactInternalInstance$` 查找相应的 fiberNode,通过递归查找到对应的 React 组件实例。找到一个挂载着 `Symbol('_LCNodeId')` 的实例,也就是上面我们初始化添加的属性。 + +通过 `Symbol('_LCNodeId')` 属性,我们可以获取 Node 的 id,这样我们就可以找到 Node 实例。 + +通过 `getBoundingClientRect` 我们可以获取到 Node 渲染出来的 DOM 的相关信息,包括 `x`、`y`、`width`、`height` 等。 + +通过 DOM 信息,我们将 focus 节点所需的标志渲染到对应的地方。hover、拖拽占位符、resize handler 等辅助 UI 都是类似逻辑。 + +#### 通信机制 + +既然设计器和渲染器处于两个 Frame,它们之间的事件通信、方法调用是通过各自的代理对象进行的,不允许其他方式,避免代码耦合。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01hxtg7X1M3AZsAdt83_!!6000000001378-2-tps-1290-648.png) + +##### host +host 可以访问设计器的所有模块,由于 renderer 层不负责与设计器相关的交互。所以增加了一层 host,作为通信的中间层。host 可以访问到设计器中所有模块,并提供相关方法供 simulator-renderer 层调用。例如 schema 的获取、组件获取等。 + +simulator-renderer 通过调用 host 的方法,将 schema、components 等参数传给 renderer,让 renderer 进行渲染。 + +##### xxx-simulator-renderer + +为了完成双向交互,simulator-renderer 也需要提供一些方法来供 host 层调用,之后当设计器和用户有交互,例如上述提到的节点选中。这里需要提供的方法有: + +- getClientRects +- getClosestNodeInstance +- findDOMNodes +- getComponent +- setNativeSelection +- setDraggingState +- setCopyState +- clearState + +这样,host 和 simulator-renderer 之间便通过相关方法实现了双向通信,能在隔离设计器的基础上完成设计器到画布和画布到设计器的通信流程。 + +### 编排辅助的核心 +#### 设置面板与设置器 +当在渲染画布上点击一个 DOM 节点,我们可以通过 xxx-simulator-renderer 获取 `Node` 节点,我们在 `Node` 上挂载了 `ComponentMeta` 实例。通过 `ComponentMeta` 我们获取到当前组件的描述模型。通过描述模型,我们即可获得组件、即当前 Node 支持的所有属性配置。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01c7nkoo1OXyRhVAFlK_!!6000000001716-2-tps-1500-985.png) + +##### 设置面板 + +设置面板对于配置项的呈现结构是通过 `ComponentMeta.configure` 来确定的。 + +```json +{ + "component": { + "isContainer": true + }, + "props": { + "isExtends": true, + "override": [ + { + "name": "count", + "title": { + "label": "展示的数字", + "tip": "count|大于 overflowCount 时显示为 ${overflowCount}+,为 0 时默认隐藏", + "docUrl": "https://fusion.alibaba-inc.com/pc/component/basic/badge" + }, + "setter": { + "componentName": "MixedSetter", + "props": { + "setters": [ + "StringSetter", + "ExpressionSetter" + ] + } + } + } + ] + } +} +``` + +上述的 `component.isContainer` 描述了这个组件是否是一个容器组件。而 props 下的属性就是我们在设置面板中展示的属性,包含了这个属性的名称、使用的设置器、配置之后影响的是哪个属性等。 + +而这只是描述,编排模块的 `SettingTopEntry` 便是管理设置面板的实现模块。 + +`SettingTopEntry` 包含了 n 个 `SettingField`,每一个 `SettingField` 就对应下面要将的设置器。即 `SettingTopEntry` 负责管理多个 `SettingField`。 + +##### 设置器 +选中节点可供配置的属性都有相应的设置器配置,比如文本、数字、颜色、JSON、Choice、I18N、表达式 等等,或者混合多种。 + +设置器本质上是一个 React 组件,但是设置面板在渲染时会传入当前配置项对应的 `SettingField` 实例,`SettingField` 本质上就是包裹了 `Prop` 实例,设置器内部的行为以及 UI 变化都由设置器自己把控,但当属性值发生变化时需要通过 `SettingField` 下的 `Prop` 来修改值,因为修改 `Prop` 实例就相当于修改了 schema。一方面这样的设置器设置之后,保存的 schema 才是正确的,另外一方面,只有 schema 变化了,才能触发渲染画布重新渲染。 + +#### 拖拽引擎 & 拖拽定位机制 + +![](https://img.alicdn.com/imgextra/i4/O1CN01G8zyBw1OkL8m0FG4J_!!6000000001743-1-tps-1425-917.gif) + +拖拽引擎(`Dragon`)核心完成的工作是将被拖拽对象拖拽到目标位置,涉及到几个概念: + +- 被拖拽对象 - `DragObject` +- 拖拽到的目标位置 - `DropLocation` +- 拖拽感应区 - `IPublicModelSensor` +- 定位事件 - `LocateEvent` + +##### Sensor + +在引擎初始化的时候,我们监听 `document` 和 iframe `contentDocument` 的 `mouse`、`keyboard`、`drag` 事件来感知拖拽的发生。而这些监听的区域我们又称为拖拽感应区,也就是 `Sensor`。`Sensor` 会有多个,因为感应器有多个,默认设置器和设置面板是没有 `Sensor`,但是他们是可以注册 `Sensor` 来增加感应区域,例如大纲树就注册了自己的 `Sensor`。 + +`Sensor` 有两个关键职责: +1. 用于事件对象转换,比如坐标系换算。 +2. 根据拖拽过程中提供的位置信息,结合每一层 `Node` 也就是组件包含的描述信息,知道其是否能作为容器等限制条件,来进行进一步的定位,最后计算出精准信息来进行视图渲染。 + +**拖拽流程** +1. 在引擎初始化的时候,初始化多个 `Sensor`。 +2. 当拖拽开始的时候,开启 `mousemove`、`mouseleave`、`mouseover` 等事件的监听。 +3. 拖拽过程中根据 `mousemove` 的 `MouseEvent` 对象封装出 `LocateEvent` 对象,继而交给相应 `sensor` 做进一步定位处理。 +4. 拖拽结束时,根据拖拽的结果进行 schema 变更和视图渲染。 +5. 最后关闭拖拽开始时的事件监听。 + +##### 拖拽方式 +根据拖拽的对象不同,我们将拖拽分为几种方式: +1. **画布内拖拽:**此时 sensor 是 simulatorHost,拖拽完成之后,会根据拖拽的位置来完成节点的精确插入。 +2. **从组件面板拖拽到画布**:此时的 sensor 还是 simulatorHost,因为拖拽结束的目标还是画布。 +3. **大纲树面板拖拽到画布中**:此时有两个 sensor,一个是大纲树,当我们拖拽到画布区域时,画布区域内的 simulatorHost 开始接管。 +4. **画布拖拽到大纲树中**:从画布中开始拖拽时,最新生效的是 simulatorHost,当离开画布到大纲树时,大纲树 sensor 开始接管生效。当拖拽到大纲树的某一个节点下时,大纲树会将大纲树中的信息转化为 schema,然后渲染到画布中。 +### 其他 + +引擎的编排能力远远不止上述所描述的功能,这里只描述了其核心和关键的功能。在整个引擎的迭代和设计过程中还有很多细节来使我们的引擎更好用、更容易扩展。 + +#### schema 处理的管道机制 + +通过 PropsReducer 的管道机制,用户可以定制自己需要的逻辑,来修改 Schema。 + +#### 组件 metadata 处理的管道机制 + +组件的描述信息都收拢在各自的 ComponentMeta 实例内,涉及到的消费方几乎遍及整个编排过程,包括但不限于 组件拖拽、拖拽辅助 UI、设置区、原地编辑、大纲树 等等。 + +在用户需要自定义的场景,开放 ComponentMeta 的修改能力至关重要,因此我们设计了 metadata 初始化/修改的管道机制。 + +#### hotkey & builtin-hotkey + +快捷键的实现,以及引擎内核默认绑定的快捷键行为。 + +#### drag resize 引擎 + +对于布局等类型的组件,支持拖拽改变大小。resize 拖拽引擎根据组件 ComponentMeta 声明来开启,拖拽后,触发组件的钩子函数(`onResizeStart` / `onResize` / `onResizeEnd`),完成 resize 过程。 + +#### OffsetObserver + +设计态的辅助 UI 需要根据渲染态的视图变化而变化,比如渲染容器滚动了,此时通过 OffsetObserver 做一个动态的监听。 + +#### 插件机制 + +我们希望保持引擎内核足够小,但拥有足够强的扩展能力,所有扩展功能都通过插件机制来承载。 diff --git a/docs/docs/guide/design/generator.md b/docs/docs/guide/design/generator.md new file mode 100644 index 0000000000..2310cb7a5f --- /dev/null +++ b/docs/docs/guide/design/generator.md @@ -0,0 +1,118 @@ +--- +title: 出码模块设计 +sidebar_position: 5 +--- + +本篇主要讲解了出码模块实现的基本思路与一些概念。如需接入出码和定制出码方案,可以参考《[使用出码功能](/site/docs/guide/expand/runtime/codeGeneration)》一节。 + +## npm 包与仓库信息 + +| **NPM 包** | **代码仓库** | **说明** | +| --- | --- | --- | +| [@alilc/lowcode-code-generator](https://www.npmjs.com/package/@alilc/lowcode-code-generator) | [alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine)(子目录:modules/code-generator)| 出码模块核心库,支持在 node 环境下运行,也提供了浏览器下运行的 standalone 模式 | +| [@alilc/lowcode-plugin-code-generator](https://www.npmjs.com/package/@alilc/lowcode-plugin-code-generator) | [alibaba/lowcode-code-generator-demo](https://github.com/alibaba/lowcode-code-generator-demo) | 出码示例 -- 浏览器端出码插件 | + +## 出码模块原理 + +出码模块的输入和输出很简单: +![](https://img.alicdn.com/imgextra/i3/O1CN01OkDmKq1xMX6Xxv6co_!!6000000006429-0-tps-1262-346.jpg) + +这里有几个概念: + +- schema: 搭建协议内容,指符合《阿里巴巴中后台前端搭建协议规范》的 schema +- solution:出码方案,指具体的项目框架(如 Rax,Ice.js) +- Source Codes:生成的源代码,以目录树的形式进行描述 + +可以看出,这是一个与用户基本没有交互,通过既定的流程完成整个功能链路的模块。其核心暴露的是一个将搭建协议 schema 按既定的 solution 转换为代码的函数。对于使用者来说就是一个输入输出都确定的黑盒系统。 + +### 出码流程概述 + +出码模块和编译器很类似,都是将代码的一种表现形式转换成另一种表现形式,如: + +#### 编译器流程 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN019F21Lb1bsCwvNcWRq_!!6000000003520-2-tps-3228-492.png) + +#### 出码模块流程 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01SEcVta1uLD72W0URZ_!!6000000006020-2-tps-1536-182.png) + +### 出码流程详解 +#### 协议解析 + +协议解析主要是将输入的 schema 解析成更适合出码模块内部使用的数据结构的过程。这样在后面的代码生成过程中就可以直接用这些数据,不必重复解析了。 + +![](https://img.alicdn.com/imgextra/i3/O1CN016EeitG1giCNCNTLVF_!!6000000004175-0-tps-1282-515.jpg) + +主要步骤如下: + +- 解析三方组件依赖 +- 分析 ref API 的使用情况 +- 建立容器之间的依赖关系索引 +- 分析容器内的组件依赖关系 +- 分析路由配置 +- 分析 utils 和 NPM 包依赖关系 +- 其他兼容处理 + +#### 前置优化 + +前置优化是计划基于策略对 schema 做一些优化。 + +主要逻辑分为分析、规则和优化三个部分,组合为一个支持通过配置进行一定程度定制化的策略包。每个策略包会先执行分析器,对输入进行特征提取,然后通过规则对特征进行判断,决定是否执行优化动作: + +![](https://img.alicdn.com/imgextra/i4/O1CN01P0Lw7v1lfyWtfQTuR_!!6000000004847-2-tps-994-278.png) + +#### 代码生成 +代码生成的流程如下: +![](https://img.alicdn.com/imgextra/i1/O1CN01lhcWBg1RG3nsoSoY2_!!6000000002083-2-tps-1468-464.png) + +如果简单粗暴地拼字符串生成源代码将难以扩展和维护,因此出码模块在代码生成过程中将代码进行了一些抽象化。 + +日常开发中,我们常常是基于某一个特定的项目框架,将一些配置、UI 代码、逻辑代码放到他们应该在的地方,最终形成一套可以 run 起来的业务系统。那么其实对于出码这件事,我们也可以层层拆解,**项目 -> 插槽 -> 模块 -> 文件 -> 代码块**(代码片段)。这样就能将复杂的项目产出问题,拆分为一个个相对专注且单一的代码块产出问题,同时也支持组合复用。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01vOGmBT1JaegccXDt8_!!6000000001045-2-tps-892-454.png) + +注:中间表达结构即为对 Schema 解析后的结构化产物 + +##### 插槽 + +首先来看下插槽,插槽描述了对应模块在项目中相对路径,并且可以对模块做固定的命名。每个插槽都有一系列插件来完成代码产出工作。生成的一个或多个文件,最终会依照插槽的描述放入项目中。 + +```typescript +// 项目模版 +export interface IProjectTemplate { + slots: Record; +} + +// 插槽 +interface IProjectSlot { + path: string[]; + fileName?: string; +} + +// 插槽出码插件配置 +interface IProjectPlugins { + [slotName: string]: BuilderComponentPlugin[]; +} +``` +##### 代码块 + +代码块是出码产物的最小单元,由出码模块插件产出,多个代码块最后会被组装为代码文件。每个代码块通过 name 描述自己,再通过 linkAfter 描述应该跟在哪些 name 的代码块后面。 + +```typescript +interface ICodeChunk { + type: ChunkType; // 处理类型 ast | string | json + fileType: string; // 文件类型 js | css | ts ... + name: string; // 代码块名称,与 linkAfter 相关 + subModule?: string; // 模块内文件名,默认是 index + content: ChunkContent; // 代码块内容,数据格式与 type 相关 + linkAfter: string[]; +} +``` + +#### 后置优化 + +后置优化分为文件级别和项目级别两种: + +- 文件级别:在生成完一个文件后进行处理 +- 项目级别:在所有文件都生成完了之后进行处理 + +文件级别的后置优化目前主要是有 prettier 这个代码格式化工具。 diff --git a/docs/docs/guide/design/materialParser.md b/docs/docs/guide/design/materialParser.md new file mode 100644 index 0000000000..78936011fd --- /dev/null +++ b/docs/docs/guide/design/materialParser.md @@ -0,0 +1,80 @@ +--- +title: 入料模块设计 +sidebar_position: 2 +--- +## 介绍 +入料模块负责物料接入,通过自动扫描、解析源码组件,产出一份符合《中后台低代码组件描述协议》的** **JSON Schema。这份 Schema 包含基础信息和属性描述信息部分,低代码引擎会基于它们在运行时自动生成一份 configure 配置,用作设置面板展示。 + +## npm 包与仓库信息 + +- npm 包:@alilc/lowcode-material-parser +- 仓库:[https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) 下的 modules/material-parser + +## 原理 +入料模块使用动静态分析结合的方案,动态胜在真实,静态胜在细致,不过全都依赖源码中定义的属性,若未定义,或者定义错误,则无法正确入料。 + +### 整体流程 +大体分为本地化、扫描、解析、转换、校验 5 部分,如下图所示。 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01sXf5fL1E5RcRxAlM1_!!6000000000300-2-tps-2116-206.png) + +### 静态解析 +在静态分析时,分为 JS 和 TS 两种情况。 + +#### 静态解析 JS +在 JS 情况下,基于 react-docgen 进行扩展,自定义了 resolver 及 handler,前者用于寻找组件定义,后者用于解析 propTypes、defaultProps 等信息,整体流程图如下: + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01VrhkEb1R6tsntvGhV_!!6000000002063-2-tps-2176-478.png) + +react-docgen 使用 babel 生成语法树,再使用 ast-types 进行遍历去寻找组件节点及其属性类型定义。原本的 react-docgen 只能解析单文件,且不能解析 IIFE、逗号表达式等语法结构 (一般出现在转码后的代码中)。笔者对其进行改造,使之可以递归解析多文件去查找组件定义,且能够解开 IIFE,以及对逗号表达式进行转换,以方便后续的组件解析。另外,还增加了子组件解析的功能,即类似 `Button.Group = Group` 这种定义。 + +#### 静态解析 TS +在 TS 情况下,还要再细分为 TS 源码和 TS 编译后的代码。 +TS 源码中,React 组件具有类型签名;TS 编译后的代码中,dts 文件 (如有) 包含全部的 class / interface / type 类型信息。可以从这些类型信息中获取组件属性描述。整体流程图如下: + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN014lOIIy1FUvGW6wcYZ_!!6000000000491-2-tps-2280-240.png) + +react-docgen 内置了 TypeScript 的 babel 插件,所以也具备解析 interface 的能力,可惜能力有限,babel 只能解析 TS 代码,但没法做类型检查,类型处理是由 react-docgen 实现的,它对于 extends/implements/utility 的情况处理不好,并且没有类型推断,虽然可以对其功能进行完善,不过这种情况下,应该借助 TypeScript Compiler 的能力,而非自己造轮子。通过调研,发现市面上有 typescript-react-docgen 这个项目。它在底层依赖了 TypeScript,且产出的数据格式与 react-docgen 一致,所以我们选择基于它进行解析。 + +TypeScript Compiler 会递归解析某个文件中出现及引用的全部类型,当然,前提是已经定义或安装了相应的类型声明。typescript-react-docgen 会调用 TypeScript Compiler 的 API,获取每个文件输出的类型,判断其是否为 React 组件。满足下列条件之一的,会被判定为 React 组件: + +1. 获取其函数签名,如果只有一个入参,或者第一个入参名称为 props,会被判定为函数式组件; +2. 获取其 `constructor` 方法,如果其返回值包含 props 属性,会被判定为有状态组件。 + +然后,遍历组件的 props 类型,获取每个属性的类型签名字符串,比如 `(a: string) => void`。typescript-react-docgen 可以克服 react-docgen 解析 TypeScirpt 类型的问题,但是每个类型都以字符串的形式来呈现,不利于后续的解析。所以,笔者对其进行了扩展,递归解析每一层的属性值。此外,在函数式组件的判定上,笔者做了完善,会看函数的返回值是否为 `ReactElement` ,若是,才为函数式组件。 + +下面讲对于一些特殊情况的处理。 + +**循环定义** + +TypeScript 类型可以循环定义,比如下面的 JSON 类型: + +```typescript +interface Json { + [x: string]: string | number | boolean | Json | JsonArray; +} +type JsonArray = Array; +``` + +因为低代码组件描述协议中没有引用功能,而且也不方便在界面上展示出来,所以这种循环定义无需完全解析,入料模块会在检测到循环定义的时候,把类型简化为 `object` 。对于特殊的类型,如 JSON,可以用相应的 Setter 来编辑。 + +**复杂类型** +TypeScript Compiler 会将合成类型的所有属性展开,比如 `boolean | string`,会被展开为 `true | false | string`,这带来了不必要的精确,我们需要的只是 `boolean | string` 而已。当然,对于这个例子,我们很容易把它还原回 `boolean | string`,然而,对于诸如 `React.ButtonHTMLAttributes & {'data-name': string}` 这种类型,它会把 `ButtonHTMLAttributes` 中众多的属性和 `data-name` 混杂在一起,完全无法分辨,只能以展开的形式提供。这 100 多个属性,如果都放在设置面板,绝对是使用者的噩梦,所以,其结果会被简化为 `object` 。当然,即使没有 `{'data-name': string}`,`ButtonHTMLAttributes` 也是没有单独的 Setter 的,同样会被简化为 `object` 。 + +### 动态解析 + +当一个组件,使用静态解析无法入料时,会使用动态解析。 + +整体流程图如下: + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01dJ62Dm1u5de8GihG6_!!6000000005986-2-tps-2516-449.png) + +基本思想很简单,require 组件进来,然后读取其组件类上定义的 propTypes 和 defaultProps 属性。这里使用了 parse-prop-types 库,使用它的时候必须在组件之前引用,因为它会先对 prop-types 库进行修改,在每个 PropTypes 透出的函数上挂上类型,比如 string, number 等等,然后再去遍历。动态解析可以解析出全部的类型信息,因为 PropTypes 有可能引入依赖组件的一些类型定义,这在静态解析中很难做到,或者成本较高,而对于动态解析来说,都由运行时完成了。 + +##### 技术细节 + +值得注意的是,有些 js 文件里还会引入 css 文件,而且从笔者了解的情况来看,这种情况在集团内部不在少数。这种组件不配合 webpack 使用,肯定会报错,但是使用 webpack 会明显拖慢速度,所以笔者采用了 sandbox 的方式,对 require 进来的类 css 文件进行 mock。这里,笔者使用了 vm2 这个库,它对 node 自带的 vm 进行了封装,可以劫持文件中的 require 方法。因为 parse-prop-types 的修改在沙箱中会失效,所以笔者也 mock 了组件中的 prop-types 库。 + +### 整体大图 +把上述的静态解析和动态解析流程结合起来,可以得到以下大图。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01TA9lQp27QmwVT7WUC_!!6000000007792-2-tps-2658-1072.png) diff --git a/docs/docs/guide/design/renderer.md b/docs/docs/guide/design/renderer.md new file mode 100644 index 0000000000..4a8c43f329 --- /dev/null +++ b/docs/docs/guide/design/renderer.md @@ -0,0 +1,215 @@ +--- +title: 渲染模块设计 +sidebar_position: 4 +--- +## 低代码渲染介绍 + + + +基于 Schema 和物料组件,如何渲染出我们的页面?这一节描述的就是这个。 + +## npm 包与仓库信息 + +- React 框架渲染 npm 包:@alilc/lowcode-react-renderer +- 仓库:[https://github.com/alibaba/lowcode-engine](https://github.com/alibaba/lowcode-engine) 下的 + - packages/renderer-core + - packages/react-renderer + - packages/react-simulator-renderer + +## 渲染框架原理 +### 整体架构 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01i4IiSR1cMtUFXaWQq_!!6000000003587-2-tps-1686-1062.png) + +- 协议层:基于[《低代码引擎搭建协议规范》](/site/docs/specs/lowcode-spec) 产出的 Schema 作为我们的规范协议。 +- 能力层:提供组件、区块、页面等渲染所需的核心能力,包括 Props 解析、样式注入、条件渲染等。 +- 适配层:由于我们使用的运行时框架不是统一的,所以统一使用适配层将不同运行框架的差异部分,通过接口对外,让渲染层注册/适配对应所需的方法。能保障渲染层和能力层直接通过适配层连接起来,能起到独立可扩展的作用。 +- 渲染层:提供核心的渲染方法,由于不同运行时框架提供的渲染方法是不同的,所以其通过适配层进行注入,只需要提供适配层所需的接口,即可实现渲染。 +- 应用层:根据渲染层所提供的方法,可以应用到项目中,根据使用的方法和规模即可实现应用、页面、区块的渲染。 + +### 核心解析 + +这里主要解析一下刚刚提到的架构中的适配层和渲染层。 + +#### 适配层 +适配层提供的是各个框架之间的差异项。比如 `React.createElement` 和 `Rax.createElement` 方法是不同的。所以需要在适配层对 API 进行抹平。 + +##### React +```typescript +import { createElement } from 'react'; +import { + adapter, +} from '@ali/lowcode-renderer-core'; + +adapter.setRuntime({ + createElement, +}); +``` +##### Rax +```typescript +import { createElement } from 'rax'; +import { + adapter, +} from '@ali/lowcode-renderer-core'; + +adapter.setRuntime({ + createElement, +}); +``` +这时,在核心层使用的 `createElement` 会基于使用不同的 renderer 而使用不同的方法,自动适配框架所需的运行时方法。 + +所需的方法包括: + +- `setRuntime`:设置运行时相关方法 + - `Component`:组件类,参考 React 的 `Component`。 + - `PureComponent`:组件类,参考 React 的 `PureComponent`。 + - `createContext`:创建一个 `Context` 对象的方法。例如,当 React 渲染一个订阅了这个 `Context` 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 `Provider` 中读取到当前的 `context` 值。 + - `createElement`:创建 `Component` 元素,例如在 React 中即为创建 React 元素。 + - `forwardRef`:ref 转发的方法。Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。 + - `findDOMNode`:是一个访问底层 DOM 节点的方法。如果组件已经被挂载到 DOM 上,此方法会返回浏览器中相应的原生 DOM 元素。 +- `setRenderers` + - `PageRenderer`:页面渲染的方法。可以定制页面渲染的生命周期,定制导航,定制路由等。 + - `ComponentRenderer`:组件渲染的方法。 + - `BlockRenderer`:区块渲染的方法。 + +#### 渲染层 +##### React Renderer +内部的技术栈统一都是 React,大多数适配层的 API 都是按照 React 来设计的,所以对于 React Renderer 来说,需要做的不多。 + +React Renderer 的代码量很少,主要是将 React API 注册到适配层中。 + +```typescript +import React, { Component, PureComponent, createElement, createContext, forwardRef, ReactInstance, ContextType } from 'react'; +import ReactDOM from 'react-dom'; +import { + adapter, + pageRendererFactory, + componentRendererFactory, + blockRendererFactory, + addonRendererFactory, + tempRendererFactory, + rendererFactory, + types, +} from '@ali/lowcode-renderer-core'; +import ConfigProvider from '@alifd/next/lib/config-provider'; + +window.React = React; +(window as any).ReactDom = ReactDOM; + +adapter.setRuntime({ + Component, + PureComponent, + createContext, + createElement, + forwardRef, + findDOMNode: ReactDOM.findDOMNode, +}); + +adapter.setRenderers({ + PageRenderer: pageRendererFactory(), + ComponentRenderer: componentRendererFactory(), + BlockRenderer: blockRendererFactory(), + AddonRenderer: addonRendererFactory(), + TempRenderer: tempRendererFactory(), + DivRenderer: blockRendererFactory(), +}); + +adapter.setConfigProvider(ConfigProvider); +``` + +##### Rax Renderer +Rax 的大多数 API 和 React 基本也是一致的,差异点在于重写了一些方法。 +```typescript +import { Component, PureComponent, createElement, createContext, forwardRef } from 'rax'; +import findDOMNode from 'rax-find-dom-node'; +import { + adapter, + addonRendererFactory, + tempRendererFactory, + rendererFactory, +} from '@ali/lowcode-renderer-core'; +import pageRendererFactory from './renderer/page'; +import componentRendererFactory from './renderer/component'; +import blockRendererFactory from './renderer/block'; +import CompFactory from './hoc/compFactory'; + +adapter.setRuntime({ + Component, + PureComponent, + createContext, + createElement, + forwardRef, + findDOMNode, +}); + +adapter.setRenderers({ + PageRenderer: pageRendererFactory(), + ComponentRenderer: componentRendererFactory(), + BlockRenderer: blockRendererFactory(), + AddonRenderer: addonRendererFactory(), + TempRenderer: tempRendererFactory(), +}); +``` + +### 多模式渲染 +#### 预览模式渲染 +预览模式的渲染,主要是通过 Schema、components 即可完成上述的页面渲染能力。 +```typescript +import ReactRenderer from '@ali/lowcode-react-renderer'; +import ReactDOM from 'react-dom'; +import { Button } from '@alifd/next'; + +const schema = { + componentName: 'Page', + props: {}, + children: [ + { + componentName: 'Button', + props: { + type: 'primary', + style: { + color: '#2077ff' + }, + }, + children: '确定', + }, + ], +}; + +const components = { + Button, +}; + +ReactDOM.render(( + +), document.getElementById('root')); +``` + +#### 设计模式渲染(Simulator) +设计模式渲染就是将编排生成的《搭建协议》渲染成视图的过程,视图是可以交互的,所以必须要处理好内部数据流、生命周期、事件绑定、国际化等等。也称为画布的渲染,画布是 UI 编排的核心,它一般融合了页面的渲染以及组件/区块的拖拽、选择、快捷配置。 +画布的渲染和预览模式的渲染的区别在于,画布的渲染和设计器之间是有交互的。所以在这里我们新增了一层 `Simulator` 作为设计器和渲染的连接器。 +`Simulator` 是将设计器传入的 `DocumentModel` 和组件/库描述转成相应的 Schema 和 组件类。再调用 Render 层完成渲染。我们这里介绍一下它提供的能力。 +##### 整体架构 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN017cYBAp1hvJKPUVLbx_!!6000000004339-2-tps-1500-864.png) + +- `Project`:位于顶层的 Project,保留了对所有文档模型的引用,用于管理应用级 Schema 的导入与导出。 +- `Document`:文档模型包括 Simulator 与数据模型两部分。Simulator 通过一份 Simulator Host 协议与数据模型层通信,达到画布上的 UI 操作驱动数据模型变化。通过多文档的设计及多 Tab 交互方式,能够实现同时设计多个页面,以及在一个浏览器标签里进行搭建与配置应用属性。 +- `Simulator`:模拟器主要承载特定运行时环境的页面渲染及与模型层的通信。 +- `Node`:节点模型是对可视化组件/区块的抽象,保留了组件属性集合 Props 的引用,封装了一系列针对组件的 API,比如修改、编辑、保存、拖拽、复制等。 +- `Props`:描述了当前组件所维系的所有可以「设计」的属性,提供一系列操作、遍历和修改属性的方法。同时保持对单个属性 Prop 的引用。 +- `Prop`:属性模型 Prop 与当前可视化组件/区块的某一具体属性想映射,提供了一系列操作属性变更的 API。 +- `Settings`:`SettingField` 的集合。 +- `SettingField`:它连接属性设置器 `Setter` 与属性模型 `Prop`,它是实现多节点属性批处理的关键。 +- 通用交互模型:内置了拖拽、活跃追踪、悬停探测、剪贴板、滚动、快捷键绑定。 + +##### 模拟器介绍 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01GF1PMj288kxovvnK8_!!6000000007888-2-tps-1500-740.png) + +- 运行时环境:从运行时环境来看,目前我们有 React 生态、Rax 生态。而在对外的历程中,我们也会拥有 Vue 生态、Angular 生态等。 +- 布局模式:不同于 C 端营销页的搭建,中后台场景大多是表单、表格,流式布局是主流的选择。对于设计师、产品来说,是需要绝对布局的方式来进行页面研发的。 +- 研发场景:从研发场景来看,低代码搭建不仅有页面编排,还有诸如逻辑编排、业务编排的场景。 + +基于以上思考,我们通过基于沙箱隔离的模拟器技术来实现了多运行时环境(如 React、Rax、小程序、Vue)、多模式(如流式布局、自由布局)、多场景(如页面编排、关系图编排)的 UI 编排。通过注册不同的运行时环境的渲染模块,能够实现编辑器从 React 页面搭建到 Rax 页面搭建的迁移。通过注册不同的模拟器画布,你可以基于 G6 或者 mxgraph 来做关系图编排。你可以定制一个流式布局的画布,也可以定制一个自由布局的画布。 diff --git a/docs/docs/guide/design/setter.md b/docs/docs/guide/design/setter.md new file mode 100644 index 0000000000..7afbbf034f --- /dev/null +++ b/docs/docs/guide/design/setter.md @@ -0,0 +1,92 @@ +--- +title: 设置器设计 +sidebar_position: 6 +--- + +设置器,又称为 Setter,是作为物料属性和用户交互的重要途径,在编辑器日常使用中有着非常重要的作用,本文重点介绍 Setter 的设计原理和使用方式,帮助用户更好的理解 Setter。 + +在编辑器的右边区域,Setter 的区块就展现在这里,如下图: + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01qEjjoQ24QNkD42wzl_!!6000000007385-2-tps-3836-1730.png) + +其中包含 属性、样式、事件、高级: + +- 属性:展示该物料常规的属性; +- 样式:展示该物料样式的属性; +- 事件:如果该物料有声明事件,则会出现事件面板,用于绑定事件; +- 高级:两个逻辑相关的属性,**条件渲染**和**循环。** +## npm 包与仓库信息 + +- npm 包:@alilc/lowcode-engine-ext +- 仓库:[https://github.com/alibaba/lowcode-engine-ext](https://github.com/alibaba/lowcode-engine-ext) + +## 设置器模块原理 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01EAmitQ1U5TUws63AV_!!6000000002466-2-tps-1534-964.png) + +设置面板依赖于以下三块抽象 + +- 编辑器上下文 `editor`,主要包含:消息通知、插件引用等 +- 设置对象 `settingTarget`,主要包含:选中的节点、是否同一值、值的储存等 +- 设置列 `settingField`,主要和当前设置视图相关,包含视图的 `ref`、以及设置对象 `settingTarget` + +### SettingTarget 抽象 + +如果不是多选,可以直接暴露 `Node` 给到这,但涉及多选编辑的时候,大家的值通常是不一样的,设置的时候需要批量设置进去,这里主要封装这些逻辑,把多选编辑的复杂性屏蔽掉。 + +所选节点所构成的**设置对象**抽象如下: + +```typescript +interface SettingTarget { + // 所设置的节点集,至少一个 + readonly nodes: Node[]; + // 所有属性值数据 + readonly props: object; + // 设置属性值 + setPropValue(propName: string, value: any): void; + // 获取属性值 + getPropValue(propName: string): any; + // 设置多个属性值,替换原有值 + setProps(data: object): void; + // 设置多个属性值,和原有值合并 + mergeProps(data: object): void; + // 绑定属性值发生变化时 + onPropsChange(fn: () => void): () => void; +} +``` + +基于设置对象所派生的**设置目标属性**抽象如下: + +```typescript +interface SettingTargetProp extends SettingTarget { + // 当前属性名称 + readonly propName: string; + // 当前属性值 + value: any; + // 是否设置对象的值一致 + isSameValue(): boolean; + // 是否是空值 + isEmpty(): boolean; + // 设置属性值 + setValue(value: any): void; + // 移除当前设置 + remove(): void; +} +``` + +### SettingField 抽象 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01D855j01j8sg9GdtJr_!!6000000004504-2-tps-2022-402.png) + +```typescript +interface SettingField extends SettingTarget { + // 当前 Field 设置的目标属性,为 group 时此值为空 + readonly prop?: SettingTargetProp; + + // 当前设置项的 ref 引用 + readonly ref?: ReactInstance; + + // 属性配置描述传入的配置 + readonly config: SettingConfig; + // others.... +} +``` diff --git a/docs/docs/guide/design/specs.md b/docs/docs/guide/design/specs.md new file mode 100644 index 0000000000..2e8e4c195c --- /dev/null +++ b/docs/docs/guide/design/specs.md @@ -0,0 +1,89 @@ +--- +title: 协议栈简介 +sidebar_position: 1 +--- +## 什么是低代码协议 +低代码引擎体系基于三份协议来构建,分别是 [《低代码引擎搭建协议规范》](/site/docs/specs/lowcode-spec)、[《低代码引擎物料协议规范》](/site/docs/specs/material-spec)和[《低代码引擎资产包协议规范》](/site/docs/specs/assets-spec), 它们保障了低代码领域的标准化,成为了生态建设和流通的基石。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01axsOyW1s01YgXnT8z_!!6000000005703-2-tps-1888-1000.png) + +## 为什么需要协议 + +首先,我们做一个不恰当的类比,我们将低代码引擎和 JavaScript 语言做一下类别。还记得之前,大家都被浏览器兼容性支配的恐惧,特别是 IE 和其他浏览器,对上层 API 实现的不一致,导致一份代码需要运行在两端需要做适配。当浏览器 / JavaScript 相关的标准出现之后,各个浏览器进行了 API 的统一,使得我们终于可以从这部分工作中解放出来(PS:Babel 对于语言特性的转换是另一个方面的问题)。 + +而在《低代码引擎搭建协议规范》出现之前,低代码领域也有类似的问题。 + +### 概念不通 + +在交流的过程中,一些对于搭建产品的术语的不一致,导致了一些沟通成本,不管是在文章分享、技术分享、交流会上,都会有这个问题。 + +### 物料孤岛 + +由于低代码产品实现的方式不同,物料的消费方式也各不相同。这里分为两种物料,低代码物料和 ProCode 物料。 + +对于低代码物料来说,A 平台创建的物料无法使用到 B 平台上,如果想在 B 平台实现同样的物料,需要按照 B 平台的标准搭建一份物料。 + +对于 ProCode 物料来说,需要在低代码平台进行消费,是需要进行转换的,包括搭建配置项的生成、物料搭建视图等,可能还需要特殊的描述文件进行描述。由于这一层没有统一,同一份 ProCode 物料每接入一个低代码,可能需要的描述文件格式不同,转换的代码不同,使用的工具也不同。 + +### 生态隔离 + +不同低代码平台的生态体系也不相同,有的低代码平台的物料生态不错,有的低代码平台的搭建体验生态不错。但是这些利好的生态,都是无法互通的,甚至就算知道了代码也无法复用,因为底层是不一致的。对于阿里巴巴集团来说,每一个平台都创建一份自己的生态,这并不是利好的。 + +### 低水平重复建设 + +大家可能觉得,以上问题对于自己造轮子来说,其实也是有利的,因为自己得到了技术上的成长。 + +但是对于低代码的平台方,实际上更多的工作,在物料的转化、物料的生成、搭建体验的小优化、部分其他平台生态的实现。这些的技术深度其实并不高,属于低水平重复建设部分。 + +### 价值不高 + +如果每个业务都要从 0 开始做,做自己的平台,会花费大量的时间来构建底层基础设施,对业务本身而言并不是一件好事;而且前端领域的底层基础设施都大同小异,不同团队重复构建造成了极大的资源浪费。 + +这样的建设,会导致从 0 到 1 都需要花费大量的时间,往往在内部人力不足、投入有限时,产品很容易在未发展壮大的时候就面临了死亡相关的决策。 + +设想一下,如果可以开发一份全集团低代码平台都可以使用的物料,是否更有成就感呢?如果可以基于已有生态进行低代码平台的快速落地,而不是花费 1-2 年搭建一个可用的低代码平台,再验证市场。在快速的验证之后,再进行更深入的打磨,这其中的思考和技术含量是否更优于之前的模式呢? + +以 2019 年的阿里巴巴的情况举例,不同平台的低代码物料但不限于: + +1. vc-deep — vc 协议 + Deep 组件库 (阿里巴巴企业智能团队基于 Fusion Next 定制); +2. Iceluna 协议 + Fusion Next; +3. AIMake 物料; +4. vc-fusion-basic + 业务改造 — vc 协议 + Fusion Next(各业务 Fork 定制); +5. vision 魔改 + vc 协议扩展 + fusion 业务组件; +6. vc 协议 + antd; + +可以看到,各个搭建平台都需要维护一套自己的基础组件库,这是非常不合理的,对基础组件库的维护会分散开发同学完成业务目标的精力。 + +建立统一的低代码领域标准化,是百利而无一害的。于是,在阿里巴巴集团 2020 年进行了讨论,建立了搭建治理&物料流通战役,此战役便产出了上文中的协议规范,成为了低代码引擎和其生态的基础。 + +## 协议的作用 + +基于统一的协议,我们完成业务组件、区块、模板等各类物料的标准统一,各类中后台研发系统生产的物料可借助物料中心进行跨系统流通,通过丰富物料生态的共享提升各平台研发系统的效率。同时完成低代码引擎的标准统一以及低代码搭建中台能力的输出,帮助业务方快速孵化本业务域中后台研发系统。 + +### 打破物料孤岛 + +#### 物料中心 + +这里以阿里集团的前端物料中间建设为例,在《低代码引擎物料协议规范》落地之后,建立了阿里巴巴各个中后台研发平台沟通、对话的基础,物料流通的先决条件已经成熟,这个时候我们还需要一个统一的物料源,用于管理物料的上传、存储、检索、分发,一个典型的中心化架构,类似 npm 的管理,这便是我们物料中心。 + +Fusion Market 是物料中心的前身,它提供了业务组件的存储、文档展示和全局透出的功能,由于 fusion 体系在集团内的广泛使用,Fusion Market 沉淀了不少的业务组件,但是这个项目却一直不温不火,只看到业务组件数量的增加,却未看到物料流通起来。其中一个原因是,没有阿里巴巴前端委员会的背书,规范很难统一,规范如果不统一,物料就很难流通; + +在规范成立之后,物料中心也将有了建设的基础,最终于 2019 年建立了物料中心,提供了物料流通的平台能力。 + +#### 低代码基础物料 + +就像 AntD、Element 之于源码研发模式,在低代码研发模式下各个搭建平台也需要一套统一的、开箱即用的低代码基础组件库。基于低代码描述协议完成了两份低代码基础物料的建设,即“Fusion 低代码基础组件库”和“AntD 低代码基础组件库”。 + +#### 源码组件低代码化 + +将源码组件一键转化为低代码物料,符合低代码物料规范,可以在低代码平台进行流通。 +### 低代码物料中心 + +当低代码物料积累到一定的量级之后,所有的搭建平台的业务物料越来越多。这些物料通过低代码物料中心进行统一的管理和消费。 +### 设置器生态的基础 + +Snippet(组件默认搭建 schema ) 由《低代码引擎搭建协议规范》定义,低代码引擎会按照规范对组件进行渲染,Configure 由《低代码引擎物料协议规范》定义,它描述了组件的 props 以及每个 prop 对应的设置器 (Prop 配置面板),低代码引擎提供了 20+ 个内置设置器,但如果我们组件的 props 超出了引擎内置设置器的范围,就需要我们自己来开发对应设置器。 +设置器最终也慢慢形成了自己的生态,这使得开发物料更加容易,可以使用已有的生态中的设置器,进行物料配置描述。 +### 低代码引擎实现标准 + +低代码引擎是以上生态的消费端,它是实现了标准协议的低代码引擎。这是不可或缺的部分,低代码引擎这里就相当于一个标准浏览器,一方面给其他的低代码平台提供了一个 Demo,其他平台可以参考低代码引擎进行实现,满足官方协议,便也可以消费相关的物料生态和其他生态。 diff --git a/docs/docs/guide/design/summary.md b/docs/docs/guide/design/summary.md new file mode 100644 index 0000000000..38d523cac9 --- /dev/null +++ b/docs/docs/guide/design/summary.md @@ -0,0 +1,69 @@ +--- +title: 架构综述 +sidebar_position: 0 +--- +## 分层架构描述 +![image.png](https://img.alicdn.com/imgextra/i4/O1CN016l8gDo1z7zlRlW1P0_!!6000000006668-2-tps-1920-1080.png) + +我们设计了这样一套分层架构,自下而上分别是协议 - 引擎 - 生态 - 平台。 + +- 底层协议栈定义的是标准,**标准的统一让上层产物的互通成为可能**。 +- 引擎是**对协议的实现**,同时通过能力的输出,向上**支撑生态开放体系**,提供各种生态扩展能力。 +- 生态就好理解了,是基于引擎核心能力上扩展出来的,比如物料、设置器、插件等,还有工具链支撑开发体系。 +- 最后,各个平台基于引擎内核以及生态中的产品组合、衔接形成满足其需求的低代码平台。 + +**每一层都明确自身的定位,各司其职,协议不会去思考引擎如何实现,引擎也不会实现具体上层平台功能,上层平台的定制化均通过插件来实现,这些理念将会贯穿我们体系设计、实现的过程。** + +## 引擎内核简述 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01QUUVu21LjTXqY6H8I_!!6000000001335-2-tps-1920-1080.png) + +低代码引擎分为 4 大模块,入料 - 编排 - 渲染 - 出码: + +- 入料模块就是将外部的物料,比如海量的 npm 组件,按照[《低代码引擎物料协议规范》](/site/docs/specs/material-spec)进行描述。将描述后的数据通过引擎 API 注册后,在编辑器中使用。 + > **注意,这里仅是增加描述,而非重写一套,这样我们能最大程度复用 ProCode 体系已沉淀的组件。** +- 编排,本质上来讲,就是**不断在生成符合[《低代码引擎搭建协议规范》](/site/docs/specs/lowcode-spec)的页面描述,将编辑器中的所有物料,进行布局设置、组件 CRUD 操作、以及 JS / CSS 编写/ 逻辑编排 **等,最终转换成页面描述,技术细节后文会展开。 +- 渲染,顾名思义,就是**将编排生成的页面描述结构渲染成视图的过程**,视图是面向用户的,所以必须处理好内部数据流、生命周期、事件绑定、国际化等。 +- 出码,就是**将编排过程产生的符合[《低代码引擎搭建协议规范》](/site/docs/specs/lowcode-spec)的页面描述转换成另一种 DSL 或 编程语言代码的过程**。 + +## 引擎生态简述 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01LkRseZ23W31l8DPzS_!!6000000007262-2-tps-1920-1080.png) + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01PYBVfZ1hL82XPrXzX_!!6000000004260-2-tps-1920-1080.png) + +引擎生态主要分为 3 部分,物料、设置器和插件。 + +### 物料生态 + +物料是低代码平台的生产资料,没有物料低代码平台则变成了无源之水无本之木。低代码平台的物料即低代码组件。因此低代码物料生态指的是: +1. 低代码物料生产能力和规范。 +2. 对低代码物料进行统一管理的物料中心。 +3. 基于 Fusion Next 的低代码基础组件库。 + +### 设置器生态 + +对于已接入物料的属性配置,需要不同的设置器。 + +比如配置数值类型的 age,需要一个数值设置器,配置对象类型的 hobby,需要一个对象设置器,依次类推。 + +每个设置器本质上都是一个 React 组件,接受由引擎传入的参数,比如 value 和 onChange,value 是初始传入的值,onChange 是在设置器的值变化时的回传函数,将值写回到引擎中。 + +```typescript +// 一个最简单的文本设置器示例 +class TextSetter extends Component { + render() { + const { value, onChange } = this.props; + return onChange(e.target.value)} />; + } +} +``` + +大多数组件所使用的设置器都是一致或相似的。如同建设低代码基础组件库一样,设置器生态是一组基础的设置器,供大多数组件配置场景使用。 + +同时提供了设置器的定制功能。 + +### 插件生态 +低代码引擎本身只包含了最小的内核,而我们所能看到的设计器上的按钮、面板等都是插件提供的。插件是组成设计器的必要部分。 + +因此我们提供了一套官方的插件生态,提供最基础的设计器功能。帮助用户通过使用插件,快速完成自己的设计器。 diff --git a/docs/docs/guide/expand/_category_.json b/docs/docs/guide/expand/_category_.json new file mode 100644 index 0000000000..15aeb3dea1 --- /dev/null +++ b/docs/docs/guide/expand/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "扩展低代码编辑器", + "position": 2, + "collapsed": false, + "collapsible": true +} diff --git a/docs/docs/guide/expand/editor/_category_.json b/docs/docs/guide/expand/editor/_category_.json new file mode 100644 index 0000000000..52662a9d1e --- /dev/null +++ b/docs/docs/guide/expand/editor/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "扩展编辑态", + "position": 1, + "collapsed": false, + "collapsible": true +} diff --git a/docs/docs/guide/expand/editor/cli.md b/docs/docs/guide/expand/editor/cli.md new file mode 100644 index 0000000000..0577a181db --- /dev/null +++ b/docs/docs/guide/expand/editor/cli.md @@ -0,0 +1,198 @@ +--- +title: 低代码生态脚手架 & 调试机制 +sidebar_position: 10 +--- +## 脚手架简述 + +在 fork 低代码编辑器 demo 项目后,您可以直接在项目中任意扩展低代码编辑器。如果您想要将自己的组件/插件/设置器封装成一个独立的 npm 包并提供给社区,您可以使用我们的低代码脚手架建立低代码扩展。 + +> Windows 开发者请在 WSL 环境下使用开发工具 +> +> WSL 中文 doc:[https://docs.microsoft.com/zh-cn/windows/wsl/install](https://docs.microsoft.com/zh-cn/windows/wsl/install) +> +> 中文教程:[https://blog.csdn.net/weixin_45027467/article/details/106862520](https://blog.csdn.net/weixin_45027467/article/details/106862520) + + +## 脚手架功能 +### 脚手架初始化 + +```bash +npm init @alilc/element your-element-name +``` +不写 your-element-name 的情况下,则在当前目录创建。 + +> 注 1:如遇错误提示 `sh: create-element: command not found` 可先执行下述命令 +```bash +npm install -g @alilc/create-element +``` + +> 注 2:觉得安装速度比较慢的同学,可以设置 npm 国内镜像,如 +```bash +npm init @alilc/element your-element-name --registry=https://registry.npmmirror.com +``` + +选择对应的元素类型,并填写对应的问题,即可完成创建。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01LAaw2R1veHDYUzGB1_!!6000000006197-2-tps-676-142.png) + +### 脚手架本地环境调试 + +```bash +cd your-element-name +npm install +npm start +``` + +### 脚手架构建 + +```bash +npm run build +``` + +### 脚手架发布 + +修改版本号后,执行如下指令即可: + +```bash +npm publish +``` + +## 🔥🔥🔥 在低代码项目中调试物料/插件/设置器 + +> 📢📢📢 低代码生态脚手架提供的调试利器,在启动 setter/插件/物料 项目后,直接在已有的低代码平台就可以调试,不需要 npm link / 手改 npm main 入口等传统方式,轻松上手,强烈推荐使用!! + +### 组件/插件/Setter 侧 + +1. 插件/setter 在原有 alt 的配置中添加相关的调试配置 + ```json + // build.json 中 + { + "plugins": [ + [ + "@alilc/build-plugin-alt", + { + "type": "plugin", + "inject": true, // 开启注入调试 + // 配置要打开的页面,在注入调试模式下,不配置此项的话不会打开浏览器 + // 支持直接使用官方 demo 项目:https://lowcode-engine.cn/demo/index.html + "openUrl": "https://lowcode-engine.cn/demo/index.html?debug" + } + ], + ] + } + ``` + +2. 组件需先安装 @alilc/build-plugin-alt,再将组件内的 `build.lowcode.js`文件修改如下 + ```javascript + const { library } = require('./build.json'); + + module.exports = { + alias: { + '@': './src', + }, + plugins: [ + [ + // lowcode 的配置保持不变,这里仅为示意。 + '@alifd/build-plugin-lowcode', + { + library, + engineScope: "@alilc" + }, + ], + [ + '@alilc/build-plugin-alt', + { + type: 'component', + inject: true, + library, + // 配置要打开的页面,在注入调试模式下,不配置此项的话不会打开浏览器 + // 支持直接使用官方 demo 项目:https://lowcode-engine.cn/demo/index.html + openUrl: "https://lowcode-engine.cn/demo/index.html?debug" + } + ]], + }; + ``` + +3. 本地组件/插件/Setter正常启动调试,在项目的访问地址增加 debug,即可开启注入调试。 + ```url + https://lowcode-engine.cn/demo/demo-general/index.html?debug + ``` + +### 项目侧的准备 + +> 如果你的低代码项目 fork 自官方 demo,那么项目侧的准备已经就绪,不用再看以下内容~ + +1. 安装 @alilc/lowcode-plugin-inject + ```bash + npm i @alilc/lowcode-plugin-inject --save-dev + ``` + +2. 在引擎初始化侧引入插件 + ```typescript + import Inject, { injectAssets } from '@alilc/lowcode-plugin-inject'; + import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + + export default async () => { + // 注意 Inject 插件必须在其他插件前注册,且所有插件的注册必须 await + await plugins.register(Inject); + await plugins.register(OtherPlugin); + await plugins.register((ctx: IPublicModelPluginContext) => { + return { + name: "editor-init", + async init() { + // 设置物料描述前,使用插件提供的 injectAssets 进行处理 + const { material, project } = ctx; + material.setAssets(await injectAssets(assets)); + }, + }; + }); + } + ``` + +3. 在 saveSchema 时过滤掉插入的 url,避免影响渲染态 + ```typescript + import { filterPackages } from '@alilc/lowcode-plugin-inject'; + export const saveSchema = async () => { + // ... + const packages = await filterPackages(editor.get('assets').packages); + window.localStorage.setItem( + 'packages', + JSON.stringify(packages), + ); + // ... + }; + ``` + +4. 如果希望预览态也可以注入调试组件,则需要在 preview 逻辑里插入组件 + ```javascript + import { injectComponents } from '@alilc/lowcode-plugin-inject'; + + async function init() { + // 在传递给 ReactRenderer 前,先通过 injectComponents 进行处理 + const components = await injectComponents(buildComponents(libraryMap, componentsMap)); + // ... + } + ``` + +注:若控制台出现如下错误,直接访问一次该 url 即可~ + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01cvKmeK1saCqpIxbLW_!!6000000005782-2-tps-1418-226.png) + + +## Meta 信息 +meta 信息是放在生态元素 package.json 中的一小段 json,用户可以通过 meta 了解到这个元素的一些基本信息,如元素类型,一些入口信息等。 + +```typescript +interface LcMeta { + type: 'plugin' | 'setter' | 'component'; // 元素类型,尚未实现 + pluginName: string; // 插件名,仅插件包含 + meta: { + dependencies: string[]; // 插件依赖的其他插件列表,仅插件包含 + engines: { + lowcodeEngine: string; // 适配的引擎版本 + } + prototype: string; // 物料描述入口,仅组件包含,尚未实现 + prototypeView: string; // 物料设计态入口,仅组件包含,尚未实现 + } +} +``` diff --git a/docs/docs/guide/expand/editor/graph.md b/docs/docs/guide/expand/editor/graph.md new file mode 100644 index 0000000000..a45f34baf0 --- /dev/null +++ b/docs/docs/guide/expand/editor/graph.md @@ -0,0 +1,155 @@ +--- +title: 图编排扩展 +sidebar_position: 8 +--- +## 项目运行 +### 前置准备 +1. 参考 https://lowcode-engine.cn/site/docs/guide/quickStart/start +2. 参考至Demo下载 https://lowcode-engine.cn/site/docs/guide/quickStart/start#%E4%B8%8B%E8%BD%BD-demo +### 选择demo-graph-x6 +在根目录下执行: +```bash +cd demo-graph-x6 +``` +### 安装依赖 +在 lowcode-demo/demo-graph-x6目录下执行: +```bash +npm install +``` +### 启动Demo +在 lowcode-demo/demo-graph-x6 目录下执行: +```bash +npm run start +``` +之后就可以通过 http://localhost:5556/ 来访问我们的 DEMO 了。 + +## 认识Demo +这里的Demo即通过图编排引擎加入了几个简单的物料而来,已经是可以面向真是用户的产品界面。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN016TbCI31hM2sJy8qkR_!!6000000004262-2-tps-5120-2726.png) +### 区域组成 +#### 顶部:操作区​ +- 右侧:保存到本地、重置页面、自定义按钮 +#### 顶部:工具区 +- 左侧:删除、撤销、重做、放大、缩小 +#### 左侧:面板与操作区​ +- 物料面板:可以查找节点,并在此拖动节点到编辑器画布中 +#### 中部:可视化页面编辑画布区域​ +- 点击节点/边在右侧面板中能够显示出对应组件的属性配置选项 +- 拖拽修改节点的排列顺序 +#### 右侧:组件级别配置​ +- 选中的组件:从页面开始一直到当前选中的节点/边位置,点击对应的名称可以切换到对应的节点上 +- 选中组件的配置:属性:节点的基础属性值设置 + +> 每个区域的组成都可以被替换和自定义来生成开发者需要的业务产品。 + +## 目录介绍 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01Luc8gr1tLq5QTbpb9_!!6000000005886-0-tps-832-1522.jpg) + +- public:与其他demo保持一致,均是lowcode engine所必要依赖 +- src + - plugins::自定义插件,完成了x6的切面回调处理功能 + - services:mock数据,真实场景中可能为异步获取数据 + +## 开发插件 +```typescript +function pluginX6DesignerExtension(ctx: IPublicModelPluginContext) { + return { + init() { + // 获取 x6 designer 内置插件的导出 api + const x6Designer = ctx.plugins['plugin-x6-designer'] as IDesigner; + + x6Designer.onNodeRender((model, node) => { + // @ts-ignore + // 自定义 node 渲染逻辑 + const { name, title } = model.propsData; + node.attr('text/textWrap/text', title || name); + }); + + x6Designer.onEdgeRender((model, edge) => { + // @ts-ignore + const { source, target, sourcePortId, targetPortId } = model.propsData; + console.log(sourcePortId, targetPortId); + requestAnimationFrame(() => { + edge.setSource({ cell: source, port: sourcePortId }); + edge.setTarget({ cell: target, port: targetPortId }); + }); + + // https://x6.antv.vision/zh/docs/tutorial/intermediate/edge-labels x6 标签模块 + // appendLabel 会触发 onEdgeLabelRender + edge.appendLabel({ + markup: Markup.getForeignObjectMarkup(), + attrs: { + fo: { + width: 120, + height: 30, + x: -60, + y: -15, + }, + }, + }); + }); + + x6Designer.onEdgeLabelRender((args) => { + const { selectors } = args + const content = selectors.foContent as HTMLDivElement + if (content) { + ReactDOM.render(
自定义 react 标签
, content) + } + }) + } + } +} + +pluginX6DesignerExtension.pluginName = 'plugin-x6-designer-extension'; + +export default pluginX6DesignerExtension; +``` +x6Designer为图实例暴露出来的一些接口,可基于此进行一些图的必要插件的封装,整个插件的封装完全follow低代码引擎的插件,详情可参考 https://lowcode-engine.cn/site/docs/guide/expand/editor/pluginWidget + +## 开发物料 +```bash +npm init @alilc/element your-material-demo +``` +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01DCCqO82ADuhS8ztCt_!!6000000008170-2-tps-546-208.png) + +仓库初始化完成 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01qK2rUe1JNpdqbdhoW_!!6000000001017-0-tps-5120-2830.jpg) + +接下来即可编写物料内容了 +图物料与低代码的dom场景存在画布的差异,因此暂不支持物料单独调试,须通过项目demo进行物料调试 + +### 资产描述 +```bash +npm run lowcode:build +``` +如果物料是个React组件,则在执行上述命令时会自动生成对应的meta.ts,但图物料很多时候并非一个React组件,因此须手动生产meta.ts + +可参考: https://github.com/alibaba/lowcode-materials/blob/main/packages/graph-x6-materials/lowcode/send-email/meta.ts +同时会自动生成物料描述文件 + +### 物料调试 +#### 物料侧 +物料想要支持被项目动态inject调试,须在build.lowcode.js中加入 +```javascript +[ + '@alilc/build-plugin-alt', + { + type: 'component', + inject: true, + library + }, +] +``` +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01HyXfL12992sDkOmOg_!!6000000008024-0-tps-5120-2824.jpg) + +本地启动 +```bash +npm run lowcode:dev +``` +#### 项目侧 +通过@alilc/lce-graph-core加载物料的天然支持了debug,因此无须特殊处理。 +若项目中自行加载,则参考 https://lowcode-engine.cn/site/docs/guide/expand/editor/cli +项目访问地址后拼接query "?debug"即可进入物料调试 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01ke58hT1aRoYJzkutk_!!6000000003327-2-tps-5120-2790.png) + + diff --git a/docs/docs/guide/expand/editor/material.md b/docs/docs/guide/expand/editor/material.md new file mode 100644 index 0000000000..6e4979553b --- /dev/null +++ b/docs/docs/guide/expand/editor/material.md @@ -0,0 +1,292 @@ +--- +title: 物料扩展 +sidebar_position: 1 +--- +## 物料简述 +物料是页面搭建的原料,按照粒度可分为组件、区块和模板: + +1. 组件:组件是页面搭建最小的可复用单元,其只对外暴露配置项,用户无需感知其内部实现; +2. 区块:区块是一小段符合低代码协议的 schema,其内部会包含一个或多个组件,用户向设计器中拖入一个区块后可以随意修改其内部内容; +3. 模板:模板和区块类似,也是一段符合低代码协议的 schema,不过其根节点的 componentName 需固定为 Page,它常常用于初始化一个页面; + +低代码编辑器中的物料需要进行一定的配置和处理,才能让用户在低代码平台使用起来。这个过程中,需要一份一份配置文件,也就是资产包。资产包文件中,针对每个物料定义了它们在低代码编辑器中的使用描述。 +## 资产包配置 +### 什么是低代码资产包 +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01SQJfxh1Y8uwDXksaK_!!6000000003015-2-tps-3068-1646.png) +在低代码 Demo 中,我们可以看到,组件面板不只提供一个组件,组件是以集合的形式提供给低代码平台的,而低代码资产包正是这些组件构成集合的形式。 +**_它背后的 Interface,_**[**_在引擎中的定义摘抄如下_**](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/assets.ts)**_:_** + +```typescript +export interface Assets { + version: string; // 资产包协议版本号 + packages?: Array; // 大包列表,external 与 package 的概念相似,融合在一起 + components: Array | Array; // 所有组件的描述协议列表 + sort: ComponentSort; // 新增字段,用于描述组件面板中的 tab 和 category +} + +export interface ComponentSort { + groupList?: String[]; // 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"] + categoryList?: String[]; // 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列; +} + +export interface RemoteComponentDescription { + exportName: string; // 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; + url: string; // 组件描述的资源链接; + package: { // 组件 (库) 的 npm 信息; + npm: string; + } +} +``` +资产包协议 TS 描述 +### Demo 中的资产包 +在 Demo 项目中,自带了一份默认的资产包: +> [https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/services/assets.json](https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/services/assets.json) + +这份资产包里的物料是我们内部沉淀出的,用户可以通过这套资产包体验引擎提供的搭建、配置能力。 +**_在项目中正常注册资产包:_** +```typescript +import { material } from '@alilc/lowcode-engine'; +// 以任何方式引入 assets +material.setAssets(assets); +``` +**_以支持调试的方式注册资产包:_** +> 这样启动并部署出来的项目,可以通过在预览地址加上 ?debug 来调试本地物料。 +> 例如: +> - 通过插件初始化一个物料 +> - 按照参考文章配置物料支持调试 +> - 启动物料 +> - 访问:[https://lowcode-engine.cn/demo/demo-general/index.html?debug](https://lowcode-engine.cn/demo/demo-general/index.html) +> +详细参考:[低代码生态脚手架 & 调试机制](https://lowcode-engine.cn/site/docs/guide/expand/editor/cli) + +```typescript +import { material } from '@alilc/lowcode-engine'; +import Inject, { injectAssets } from '@alilc/lowcode-plugin-inject'; +await material.setAssets(await injectAssets(assets)); +``` + +### 手工配置资产包 +参考 Demo 中的[基础 Fusion Assets 定义](https://github.com/alibaba/lowcode-demo/blob/main/demo-basic-fusion/src/services/assets.json),如果我们修改 assets.json,我们就能做到配置资产包: + +- packages 对象:我们需要在其中定义这个包的获取方式,如果不定义,就不会被低代码引擎动态加载并对应上组件实例。定义方式是 UMD 的包,低代码引擎会尝试在 window 上寻找对应 library 的实例; +- components 对象:我们需要在其中定义物料描述,物料描述我们将在下一节继续讲解。 +## 物料描述配置 +### 什么是物料描述 +在低代码平台中,用户是不同的,有可能是开发、测试、运营、设计,也有可能是销售、行政、HR 等等各种角色。他们大多数不具备专业的前端开发知识,对于低代码平台来说,我们使用组件的流程如下: + +1. 用户通过拖拽/选择组件,在画布中看到组件; +2. 选中组件,出现组件的配置项; +3. 修改组件配置项; +4. 画布更新生效。 + +**_当我们选中一个组件,我们可以看到面板右侧会显示组件的配置项。_** +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01T5hGcl25ABLpLIWKh_!!6000000007485-2-tps-1500-743.png) +**_它包含以下内容:_** + +1. 基础信息:描述组件的基础信息,通常包含包信息、组件名称、标题、描述等。 +2. 组件属性信息:描述组件属性信息,通常包含参数、说明、类型、默认值 4 项内容。 +3. 能力配置/体验增强:推荐用于优化搭建产品编辑体验,定制编辑能力的配置信息。 + +因此,我们设计了[**《中后台低代码组件描述协议》**](/site/docs/specs/material-spec)来描述一个低代码编辑器中可被配置的内容。 +### Demo 中的物料描述 +我们可以从 Demo 中的 assets.json 找到如下三个物料描述: + +- @alifd/pro-layout:布局组件,放在`window.AlifdProLayoutMeta`,[meta 文件地址](https://alifd.alicdn.com/npm/@alifd/pro-layout@1.0.1-beta.5/build/lowcode/meta.js); +- @alifd/fusion-ui:精选组件,放在`window.AlifdFusionUiMeta`,[meta 文件地址](https://alifd.alicdn.com/npm/@alifd/fusion-ui@1.0.5-beta.1/build/lowcode/meta.js); +- @alilc/lowcode-materials:原子组件,放在 `window.AlilcLowcodeMaterialsMeta`,[meta 文件地址](https://alifd.alicdn.com/npm/@alilc/lowcode-materials@1.0.1/build/lowcode/meta.js); + +**_引擎中,会尝试调用对应 meta 文件,并注入到全局:_** +```tsx +const src = 'https://alifd.alicdn.com/npm/@alifd/pro-layout@1.0.1-beta.5/build/lowcode/meta.js'; +const script = document.createElement('script'); +script.src = src; +document.head.appendChild(script); +``` +然后在 window 上就能拿到对应的物料描述内容了: +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01DHSEOH1RwCEq19Ro9_!!6000000002175-2-tps-1896-1138.png) +手工配置物料描述时,可以用这样的方式参考一下 Demo 中的物料描述是如何实现的。 +### 手工配置物料描述 +详见:“物料描述详解”章节。 +## 物料的低代码开发 +> _**注意:引擎提供的 cli 并未对 windows 系统做适配,windows 环境必须使用 **_[_**WSL**_](https://docs.microsoft.com/zh-cn/windows/wsl/install)_**,其他终端不保证能正常运行**_ + +您可以通过本节内容,完成一个组件在低代码编辑器中的配置和调试。 +### 前言(必读) +引擎提供的物料开发脚手架内置了**_入料模块_**,初始化的时候会自动根据源码解析出一份_**低代码描述**_,但是从源码解析出来的低代码描述让用户直接使用是不够精细的,因为源码包含的信息不够,它没办法完全包含配置项的交互; +![image.png](https://img.alicdn.com/imgextra/i1/O1CN010t0YzC1znDPQB1LUA_!!6000000006758-2-tps-802-1830.png) +比如设计师出了上面的设计稿,这里面除了有哪些 props 可被配置,通过哪个设置器配置,还包含了 props 之间的聚合、排序,甚至有自定义 setter,这些信息源码里是不具备的,需要在低代码描述里进行开发; +**_因此我们建议只把 cli 初始化的低代码描述作为启动,要根据用户习惯对配置项进行设计,然后人工地去开发调试直接的低代码描述。_** +### 新开发组件 +#### 组件项目初始化 +```bash +npm init @alilc/element your-material-name +``` +#### 选择组件类型 +> 组件 -> <组件组织方式> + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01BTiMt51iLPtzDbuh8_!!6000000004396-2-tps-1596-464.png) +这里我们选择 react-组件库,之后便生出我们的组件库项目,目录结构如下: +``` +my-materials +├── README.md +├── components (业务组件目录) +│ ├── ExampleComponent // 业务组件1 +│ │ ├── build // 【编译生成】【必选】 +│ │ │ └── index.html // 【编译生成】【必选】可直接预览文件 +│ │ ├── lib // 【编译生成】【必选】 +│ │ │ ├── index.js // 【编译生成】【必选】js 入口文件 +│ │ │ ├── index.scss // 【编译生成】【必选】css 入口文件 +│ │ │ └── style.js // 【编译生成】【必选】js 版本 css 入口文件,方便去重 +│ │ ├── demo // 【必选】组件文档,用于生成组件开发预览,以及生成组件文档 +│ │ │ └── basic.md +│ │ ├── src // 【必选】组件源码 +│ │ │ ├── index.js // 【必选】,组件出口文件 +│ │ │ └── main.scss // 【必选】,仅包含组件自身样式的源码文件 +│ │ ├── README.md // 【必选】,组件说明及API +│ │ └── package.json // 【必选】 +└── └── ExampleComponent2 // 业务组件2 +``` +#### 组件开发与调试 +```bash +# 安装依赖 +npm install + +# 启动 lowcode 环境进行调试预览 +npm run lowcode:dev + +# 构建低代码产物 +npm run lowcode:build +``` +执行上述命令后会在组件 (库) 根目录生成一个 `lowcode` 文件夹,里面会包含每个组件的低代码描述: +![image.png](https://img.alicdn.com/imgextra/i2/O1CN016m7gOK1DvpIcnlTvY_!!6000000000279-2-tps-1446-906.png) + +在 src/components 目录新增一个组件并在 src/index.tsx 中导出,然后再执行 npm run lowcode:dev 时,低代码插件会在 lowcode/ 目录自动生成新增组件的低代码描述(meta.ts)。 + +用户可以直接修改低代码描述来修改组件的配置: + +- 设置组件的 setter(上一个章节介绍的设置器,也可以定制设置器用到物料中); +- 新增组件配置项; +- 更改当前配置项; +#### 配置示例 +隐藏一个 prop +```typescript +{ + name: 'dataSource', + condition: () => false, +} +``` +展示样式 +```typescript +{ + name: 'dataSource', + display: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry', // 常用的是 inline(默认), block、entry +} +``` +#### 编辑态视图 +用户可以在 lowcode/ 目录下新增 view.tsx 来增加编辑态视图。编辑态视图用于在编辑态时展示与真实预览不一样的视图。 +view.tsx 输出的也是一个 React 组件。 + +注意:如果是单组件,而非组件库模式的话,view.tsx 应置于 lowcode 而非 lowcode/ 目录下 + + +#### 发布组件 +```bash +# 在组件根目录下,执行 +$ npm publish +``` +### 现存组件低代码化 +组件低代码化是指,在引入低代码平台之前,我们大多数都是使用源码开发的组件,也就是 ProCode 组件。 + +在引入低代码平台之后,原来的源码组件是需要转化为低代码物料,这样才能在低代码平台进行消费。 + +所以接下来会说明,对于已有的源码组件,我们如何把它低代码化。 +#### 配置低代码开发环境 +在您的组件开发环境中,安装 [build-scripts](https://github.com/ice-lab/build-scripts) 和它的低代码开发插件: +```bash +npm install -D @alifd/build-plugin-lowcode @alib/build-scripts --save-dev +``` +新增 build-scripts 配置文件:build.lowcode.js + +```javascript +module.exports = { + alias: { + '@': './src', + }, + plugins: [ + [ + "@alifd/build-plugin-lowcode", + { + engineScope: '@alilc', + } + ] + ], +}; + +``` +在 package.json 中定义低代码开发相关命令 +```javascript +"lowcode:dev": "build-scripts start --config ./build.lowcode.js", +"lowcode:build": "build-scripts build --config ./build.lowcode.js", +``` +![image.png](https://img.alicdn.com/imgextra/i2/O1CN014iSa1P1dNdkUUtoMm_!!6000000003724-2-tps-1830-822.png) +#### 开发调试 + +```bash +# 启动低代码开发调试环境 +npm run lowcode:dev +``` + +组件开发形式还和原来的保持一致,但是新增了一份组件的配置文件,其中配置方式和低代码物料的配置是一样的。 + +#### 构建 + +```bash +# 构建低代码产物 +npm run lowcode:build +``` + +#### 发布组件 +```bash +# 在组件根目录下,执行 +npm publish +``` + +## 在项目中引入组件 (库) +> 以下内容可观看[《阿里巴巴低代码引擎项目实战 (3)-自定义组件接入》](https://www.bilibili.com/video/BV1dZ4y1m76S/)直播回放 + +对于平台或者用户来说,可能所需要的组件集合是不同的。如果需要自定义组件集合,就需要定制资产包,定制的资产包是配置了一系列组件的,将这份资产包用于引擎即可在引擎中使用自定义的组件集合。 + +### 管理一份资产包 +项目中使用的组件相关资源都需要在资产包中定义,那么我们自己开发的组件库如果要在项目中使用,只需要把组件构建好的相关资源 merge 到 assets.json 中就可以; + +#### 自定义组件加入到资产包 +通过官方脚手架自定义组件构建发布之后,npm 包里会出现一个 `build/lowcode/assets-prod.json`文件,我们只需要把该文件的内容 merge 到项目的 assets.json 中就可以; + +#### 资产包托管 + +- 最简单的方式就是类似[引擎 demo 项目](https://github.com/alibaba/lowcode-demo/blob/main/demo-general/src/services/assets.json)的做法,在项目中维护一份 assets.json,新增组件或者组件版本更新都需要修改这份资产包; +- 灵活一点的做法是通过 oss 等服务维护一份远程可配置的 assets.json,新增组件或者组件更新只需要修改这份远程的资产包,项目无需更新; +- 再高级一点的做法是实现一个资产包管理的服务,能够通过用户界面去更新资产包的内容; + +### 在项目中引入资产包 +```typescript +import { material, plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +// 动态加载 assets +plugins.register((ctx: IPublicModelPluginContext) => { + return { + name: 'ext-assets', + async init() { + try { + // 将下述链接替换为您的物料即可。无论是通过 utils 从物料中心引入,还是通过其他途径如直接引入物料描述 + const res = await window.fetch('https://fusion.alicdn.com/assets/default@0.1.95/assets.json'); + const assets = await res.text(); + material.setAssets(assets); + } catch (err) { + console.error(err); + } + }, + } +}).catch(err => console.error(err)); +``` diff --git a/docs/docs/guide/expand/editor/metaSpec.md b/docs/docs/guide/expand/editor/metaSpec.md new file mode 100644 index 0000000000..dda16a9cb3 --- /dev/null +++ b/docs/docs/guide/expand/editor/metaSpec.md @@ -0,0 +1,565 @@ +--- +title: 物料描述详解 +sidebar_position: 2 +--- +## 物料描述概述 + +中后台前端体系中,存在大量的组件,程序员可以通过阅读文档,知悉组件的用法。可是搭建平台无法理解 README,而且很多时候,README 里并没有属性列表。这时,我们需要一份额外的描述,来告诉低代码搭建平台,组件接受哪些属性,又是该用怎样的方式来配置这些属性,于是,[**《中后台低代码组件描述协议》**](/site/docs/specs/material-spec)应运而生。协议主要包含三部分:基础信息、属性信息 props、能力配置/体验增强 configure。 + +物料配置,就是产出一份符合[**《中后台低代码组件描述协议》**](/site/docs/specs/material-spec)的 JSON Schema。如果需要补充属性描述信息,或需要定制体验增强部分(如修改 Setter、调整展示顺序等),就可以通过修改这份 Schema 来实现。目前有自动生成、手工配置这两种方式生成物料描述配置。 + +## 可视化生成物料描述 + +使用 Parts 造物平台:[使用文档](/site/docs/guide/expand/editor/parts/partsIntro) + +## 自动生成物料描述 + +可以使用官方提供的 `@alilc/lowcode-material-parser` 解析本地组件,自动生成物料描述。把物料描述放到资产包定义中,就能让低代码引擎理解如何制作物料。详见上一个章节“物料扩展”。 + +下面以某个组件代码片段为例: +```typescript +// /path/to/component +import { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +export default class FusionForm extends PureComponent { + static displayName = 'FusionForm'; + + static defaultProps = { + name: '张三', + age: 18, + friends: ['李四','王五','赵六'], + } + + static propTypes = { + /** + * 这是用于描述姓名 + */ + name: PropTypes.string.isRequired, + /** + * 这是用于描述年龄 + */ + age: PropTypes.number, + /** + * 这是用于描述好友列表 + */ + friends: PropTypes.array + }; + + render() { + return
dumb
; + } +} +``` + +引入 parse 工具自动解析 + +```typescript +import parse from '@alilc/lowcode-material-parser'; +(async () => { + const result = await parse({ entry: '/path/to/component' }); + console.log(JSON.stringify(result, null, 2)); +})(); +``` + +因为一个组件可能输出多个子组件,所以解析结果是个数组。 + +```json +[ + { + "componentName": "FusionForm", + "title": "", + "docUrl": "", + "screenshot": "", + "devMode": "proCode", + "npm": { + "package": "", + "version": "", + "exportName": "default", + "main": "", + "destructuring": false, + "subName": "" + }, + "props": [ + { + "name": "name", + "propType": "string", + "description": "这是用于描述姓名", + "defaultValue": "张三" + }, + { + "name": "age", + "propType": "number", + "description": "这是用于描述年龄", + "defaultValue": 18 + }, + { + "name": "friends", + "propType": "array", + "description": "这是用于描述好友列表", + "defaultValue": [ + "李四", + "王五", + "赵六" + ] + } + ] + } +] +``` + +## 手工配置物料描述 + +如果自动生成的物料无法满足需求,我们就需要手动配置物料描述。本节将分场景描述物料配置的内容。 + +### 常见配置 + +#### 组件的属性只有有限的值 + +增加一个 size 属性,只能从 'large'、'normal'、'small' 这个候选值中选择。 + +以上面自动解析的物料为例,在此基础上手工加上 size 属性: + +```json +[ + { + "componentName": "FusionForm", + "title": "", + "docUrl": "", + "screenshot": "", + "devMode": "proCode", + "npm": { + "package": "", + "version": "", + "exportName": "default", + "main": "", + "destructuring": false, + "subName": "" + }, + "props": [ + { + "name": "name", + "propType": "string", + "description": "这是用于描述姓名", + "defaultValue": "张三" + }, + { + "name": "age", + "propType": "number", + "description": "这是用于描述年龄", + "defaultValue": 18 + }, + { + "name": "friends", + "propType": "array", + "description": "这是用于描述好友列表", + "defaultValue": [ + "李四", + "王五", + "赵六" + ] + } + ], + // 手工增加的 size 属性 + "configure": { + "isExtend": true, + "props": [ + { + "title": "尺寸", + "name": "size", + "setter": { + "componentName": 'RadioGroupSetter', + "isRequired": true, + "props": { + "options": [ + { "title": "大", "value": "large" }, + { "title": "中", "value": "normal" }, + { "title": "小", "value": "small" } + ] + }, + } + } + ] + } + } +] +``` + +#### 组件的属性既可以设置固定值,也可以绑定到变量 + +我们知道一种属性形式就需要一种 setter 来设置,如果想要将 value 属性允许输入字符串,那就需要设置为 `StringSetter`,如果允许绑定变量,就需要设置为 `VariableSetter`,具体设置器请参考[预置设置器列表](/site/docs/guide/appendix/setters)。 + +那如果都想要呢?可以使用 `MixedSetter` 来实现。 + +```javascript +{ + // ... + configure: { + isExtend: true, + props: [ + { + title: '输入框的值', + name: 'activeValue', + setter: { + componentName: 'MixedSetter', + isRequired: true, + props: { + setters: [ + 'StringSetter', + 'NumberSetter', + 'VariableSetter', + ], + }, + } + } + ] + } +} +``` + +设置后,就会出现“切换设置器”的操作项了 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01jBqcuK1xYRP00WyVx_!!6000000006455-2-tps-598-252.png) + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01944xqq1PYihvYQb4v_!!6000000001853-2-tps-244-308.png) + +#### 开启组件样式设置 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01EBStyl24EvqJkAdh1_!!6000000007360-2-tps-820-772.png) + +```javascript +{ + configure: { + // ..., + supports: { + style: true, + }, + // ... + } +} +``` + +#### 设置组件的默认事件 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN012gijqt1NERwqF5f6Y_!!6000000001538-2-tps-776-800.png) + +```javascript +{ + configure: { + // ... + supports: { + events: ['onPressEnter', 'onClear', 'onChange', 'onKeyDown', 'onFocus', 'onBlur'], + }, + // ... + } +} +``` + +#### 设置 prop 标题的 tip + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01d8TdsY1jhENsKvwAv_!!6000000004579-2-tps-908-176.png) + +```javascript +{ + name: 'label', + setter: 'StringSetter', + title: { + label: { + type: 'i18n', + zh_CN: '标签文本', + en_US: 'Label', + }, + tip: { + type: 'i18n', + zh_CN: '属性:label | 说明:标签文本内容', + en_US: 'prop: label | description: label content', + }, + }, +} +``` + +#### 配置 prop 对应 setter 在配置面板的展示方式 + +##### inline + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01z1sXj420vkP7vbeHj_!!6000000006912-2-tps-790-266.png) + +```javascript +{ + configure: { + props: [{ + description: '标签文本', + display: 'inline', + }] + } +} +``` + +##### block + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01i3MVKF299xchs6kMX_!!6000000008026-2-tps-792-274.png) + +```javascript +{ + configure: { + props: [{ + description: '高级', + display: 'block', + }] + } +} +``` + +##### accordion + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01RePeyy1nhvRiBMm2w_!!6000000005122-2-tps-798-740.png) + +```javascript +{ + configure: { + props: [{ + description: '表单项配置', + display: 'accordion', + }] + } +} +``` + +##### entry + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01zkjBak1YY6igYUO1n_!!6000000003070-2-tps-796-424.png) + + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01lmuRTl1LOPKMnsfLJ_!!6000000001289-2-tps-794-632.png) + +```javascript +{ + configure: { + props: [{ + description: '风格与样式', + display: 'entry', + }] + } +} +``` + +##### plain + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01G0DOfV1jGD0v049gk_!!6000000004520-2-tps-776-438.png) + +```javascript +{ + configure: { + props: [{ + description: '返回上级', + display: 'plain', + }] + } +} +``` + + +### 进阶配置 + +#### 组件的 children 属性允许传入 ReactNode + +例如有一个如下的 Tab 选项卡组件,每个 TabPane 的 children 都是一个组件 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01Cu09HV1m8pTucSc7Q_!!6000000004910-2-tps-2332-334.png) + +只需要增加 `isContainer` 配置即可 + +```javascript +{ + // ... + configure: { + // ... + component: { + // 新增,设置组件为容器组件,可拖入组件 + isContainer: true, + }, + } +} +``` + +假设我们希望只允许拖拽 Table、Button 等内容放在 TabPane 里。配置白名单 `childWhitelist` 即可 + +```javascript +{ + // ... + configure: { + // ... + component: { + isContainer: true, + nestingRule: { + // 允许拖入的组件白名单 + childWhitelist: ['Table', 'Button'], + // 同理也可以设置该组件允许被拖入哪些父组件里 + parentWhitelist: ['Tab'], + }, + }, + }, +} +``` +#### 组件的非 children 属性允许传入 ReactNode + +这就需要使用 `SlotSetter` 开启插槽了,如下面示例,给 Tab 的 title 开启插槽,允许拖拽组件 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01P77m5m1pKEBXTk9Yt_!!6000000005341-2-tps-3016-580.png) + +```json +{ + // ... + configure: { + isExtend: true, + props: [ + { + title: '选项卡标题', + name: 'title', + setter: { + componentName: 'MixedSetter', + props: { + setters: [ + 'StringSetter', + 'SlotSetter', + 'VariableSetter', + ], + }, + }, + }, + ], + }, +} +``` + +#### 屏蔽组件在设计器中的操作按钮 + +正常情况下,组件允许复制: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01925Nyl1a2AKNQ1XCP_!!6000000003271-2-tps-1158-226.png) + +如果希望禁止组件的复制行为,我们可以这样做: + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01IoLKUu1CXGRb0ileB_!!6000000000090-2-tps-1176-300.png) + +```javascript +{ + configure: { + component: { + disableBehaviors: ['copy'], + }, + }, +} +``` + +#### 实现一个 BackwardSetter + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01GI4VfT23ga8TUCjIh_!!6000000007285-2-tps-776-438.png) + +```javascript +{ + name: 'back', + title: ' ', + display: 'plain', + setter: BackwardSetter, +} + +// BackwardSetter +import { SettingTarget, DynamicSetter } from '@alilc/lowcode-types'; +const BackwardSetter: DynamicSetter = (target: SettingTarget) => { + return { + componentName: ( + + ), + }; +}; +``` + +### 高级配置 + +#### 不展现一个 prop 配置 + +- 始终隐藏当前 prop + +```javascript +{ + // 始终隐藏当前 prop 配置 + condition: () => false, +} +``` + +- 根据其它 prop 的值展示/隐藏当前 prop + +```javascript +{ + // direction 为 hoz 则展示当前 prop 配置 + condition: (target) => { + return target.getProps().getPropValue('direction') === 'hoz'; + } +} +``` + +#### props 联动 + +```javascript +// 根据当前 prop 的值动态设置其它 prop 的值 +{ + name: 'labelAlign', + // ... + extraProps: { + setValue: (target, value) => { + if (value === 'inset') { + target.getProps().setPropValue('labelCol', null); + target.getProps().setPropValue('wrapperCol', null); + } else if (value === 'left') { + target.getProps().setPropValue('labelCol', { fixedSpan: 4 }); + target.getProps().setPropValue('wrapperCol', null); + } + return target.getProps().setPropValue('labelAlign', value); + }, + }, +} +// 根据其它 prop 的值来设置当前 prop 的值 +{ + name: 'status', + // ... + extraProps: { + getValue: (target) => { + const isPreview = target.getProps().getPropValue('isPreview'); + return isPreview ? 'readonly' : 'editable'; + } + } +} +``` + +#### 动态 setter 配置 + +可以通过 DynamicSetter 传入的 target 获取一些引擎暴露的数据,例如当前有哪些组件被加载到引擎中,将这个数据作为 SelectSetter 的选项,让用户选择: + +```javascript +{ + setter: (target) => { + return { + componentName: 'SelectSetter', + props: { + options: target.designer.props.componentMetadatas.filter( + (item) => item.isFormItemComponent).map( + (item) => { + return { + title: item.title || item.componentName, + value: item.componentName, + }; + } + ), + ), + }, + }; + } +} +``` diff --git a/docs/docs/guide/expand/editor/parts/_category_.json b/docs/docs/guide/expand/editor/parts/_category_.json new file mode 100644 index 0000000000..005a3caf6c --- /dev/null +++ b/docs/docs/guide/expand/editor/parts/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "Parts 造物", + "position": 1 +} diff --git a/docs/docs/guide/expand/editor/parts/partsIntro.md b/docs/docs/guide/expand/editor/parts/partsIntro.md new file mode 100644 index 0000000000..a6fc6e8817 --- /dev/null +++ b/docs/docs/guide/expand/editor/parts/partsIntro.md @@ -0,0 +1,18 @@ +--- +title: 介绍 +sidebar_position: 1 +--- +## 介绍 +![](https://gw.alicdn.com/imgextra/i2/O1CN01Gyq6AZ1nOENPTVXX7_!!6000000005079-2-tps-256-104.png) + + +「Parts·造物」是基于开源低代码引擎打造的次时代物料研发和集成工具,一方面作为低代码引擎搭建低代码平台的一个样板展示开源生态下的各个组件如何集合在一起形成生产力,另一方面也可以生产低代码平台所需的物料。 + +目前「Parts·造物」主要提供两大产品功能: + 1. React 组件导入低代码引擎:通过在线可视化的「物料描述」配置,任意工具开发的 React 组件都可以快速完成对低代码引擎的适配,导入到低代码引擎项目中进行使用。不必额外开发新的组件。 + 2. 低代码生产组件:通过低代码的形式生产组件,极低上手门槛,提供丰富的原子组件用于组合,完善的调试预览和组件生命周期控制。生产的组件既可以在低代码引擎项目中使用,也可以出码后在普通源码项目中使用。 + + +## 联系我们 + + diff --git a/docs/docs/guide/expand/editor/parts/partsassets.md b/docs/docs/guide/expand/editor/parts/partsassets.md new file mode 100644 index 0000000000..00670ecadc --- /dev/null +++ b/docs/docs/guide/expand/editor/parts/partsassets.md @@ -0,0 +1,267 @@ +--- +title: 资产包管理 +sidebar_position: 4 +--- + +## 介绍 + +通过前述介绍,相信大家已经了解如何使用「[Parts·造物](https://parts.lowcode-engine.cn/)」来将已有的 React 组件快速接入低代码引擎,以及生产低代码组件。 + +大家在使用的过程中,可能会希望构建出来的资产包可以后续随时访问下载,或者希望构建资产包时各个组件的版本等信息可以持久化起来并且能够多人维护。 + +通过「[Parts·造物](https://parts.lowcode-engine.cn/)」的 `资产包` 管理功能帮助大家解决这个问题 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01Fkaznh1zWj9wYKpcH_!!6000000006722-2-tps-1702-628.png) + +## 新建资产包 + +首先,我们在 我的资产包 tab 中点击 `新建资产包` +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01qe8zfO1ilysebSfD5_!!6000000004454-2-tps-3064-1432.png) + +- 填写资产包名称 +- 配置资产包管理员,管理员拥有该资产包的所有权限,初始默认为资产包的创建者,还可以添加其他人作为管理员, +- 配置资产包描述 (可选) +- 点击 `确定`, 即可完成资产包的创建 + +接下来需要为资产包添加一个或者多个组件。 + +## 添加组件 + +第二步:新建完资产包以后,我们就可以为其添加组件了,如果是新建资产包流程,新建完成之后会自动弹出组件配置的弹窗,当然,你可可以通过点击资产包卡片的方式打开组件配置的弹窗。 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01kqymdB1nkDQclPk7F_!!6000000005127-2-tps-965-261.png) + +- 点击弹窗中 `添加组件` 按钮,在弹出的组件选择面板中,选中需要添加的组件并点击 `下一步`。 + ![image.png](https://img.alicdn.com/imgextra/i1/O1CN014Baihf1r742Qi1Wel_!!6000000005583-2-tps-1856-1520.png) +- 进入组件版本以及描述协议版本选择界面,选择所需要的正确版本,点击 `安装` 即可完成一个组件的添加。 + ![image.png](https://img.alicdn.com/imgextra/i2/O1CN01Y7aWWi1MMPDVlidgz_!!6000000001420-2-tps-1668-1462.png) + +## 构建资产包 + +添加完组件以后就点击 `保存并构建资产包` 进入资产包构建配置弹窗 +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01iZf4Ue1PlXnyKYxnK_!!6000000001881-2-tps-1288-670.png) + +- `开启缓存` : 可充分利用之前的构建结果缓存来加速资产包的生成,我们会将每个组件的构建结果以 包名和版本号为 key 进行缓存。 +- `任务描述` : 当前构建任务的一些描述信息。 + +点击 `确认` 按钮 会自动跳转到当前资产包的构建历史界面: +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01krDaFc1TuTztMPssI_!!6000000002442-2-tps-1726-696.png) +构建历史界面会显示当前资产包所有的构建历史记录,表格状态栏展示了构建的状态:`成功`,`失败`,`正在运行` 三种状态,操作列可以在构建成功时复制或者下载资产包结果 + +## 使用资产包 +你可以在 [lowcode-demo](https://github.com/alibaba/lowcode-demo) 中直接引用,可直接替换 demo 中原来的资产包文件: +例如,在 [demo-lowcode-component](https://github.com/alibaba/lowcode-demo/tree/main/demo-lowcode-component) 中,直接用你的资产包文件替换文件[assets.json](https://github.com/alibaba/lowcode-demo/blob/main/demo-lowcode-component/src/services/assets.json),即可快速使用自己的物料了。 + +### 在编辑器中使用资产包 +在使用含有低代码组件的资产包注意 注意引擎版本必须大于等于 `1.1.0-beta.9`。 +然后直接替换 [lowcode-demo](https://github.com/alibaba/lowcode-demo) demo 中的 `assets.json` 文件即可。 + +### 在预览中使用资产包 +在预览中使用资产包的整体思路是从 `资产包` 中提取并转换出 `ReactRenderer` 渲染所需要的 react 组件列表 (`components` 参数),然后将 `schema` 以及 `components` 传入到 `ReactRenderer` 中进行渲染,需要注意的是,在 `资产包` 的转换过程中,我们也需要将 `低代码组件` 转换成 react 组件,具体逻辑可以参考下 [demo-lowcode-component](https://github.com/alibaba/lowcode-demo/tree/main/demo-lowcode-component) 中 `src/parse-assets.ts` 文件的实现。 +基于资产包进行预览的整体逻辑如下: [详见](https://github.com/alibaba/lowcode-demo/blob/main/demo-lowcode-component/src/preview.tsx): +```ts +import ReactDOM from 'react-dom'; +import React, { useState } from 'react'; +import { Loading } from '@alifd/next'; +import ReactRenderer from '@alilc/lowcode-react-renderer'; +import { createFetchHandler } from '@alilc/lowcode-datasource-fetch-handler'; +import { + getProjectSchemaFromLocalStorage, +} from './services/mockService'; +import assets from './services/assets.json'; +import { parseAssets } from './parse-assets'; + +const getScenarioName = function () { + if (location.search) { + return new URLSearchParams(location.search.slice(1)).get('scenarioName') || 'index'; + } + return 'index'; +}; + +const SamplePreview = () => { + const [data, setData] = useState({}); + async function init() { + const scenarioName = getScenarioName(); + const projectSchema = getProjectSchemaFromLocalStorage(scenarioName); + const { componentsMap: componentsMapArray, componentsTree } = projectSchema; + const schema = componentsTree[0]; + const componentsMap: any = {}; + componentsMapArray.forEach((component: any) => { + componentsMap[component.componentName] = component; + }); + + // 特别提醒重点注意!!!:从资产包中解析出所有的 react 组件列表 + const { components } = await parseAssets(assets); + + setData({ + schema, + components, + }); + } + + const { schema, components } = data; + + if (!schema || !components) { + init(); + return ; + } + + return ( +
+ +
+ ); +}; + +ReactDOM.render(, document.getElementById('ice-container')); +``` + +从资产包中解析 react 组件列表的逻辑如下,[详见](https://github.com/alibaba/lowcode-demo/blob/main/demo-lowcode-component/src/parse-assets.ts): +```ts +import { ComponentDescription, ComponentSchema, RemoteComponentDescription } from '@alilc/lowcode-types'; +import { buildComponents, AssetsJson, AssetLoader } from '@alilc/lowcode-utils'; +import ReactRenderer from '@alilc/lowcode-react-renderer'; +import { injectComponents } from '@alilc/lowcode-plugin-inject'; +import React, { createElement } from 'react'; + +export async function parseAssets(assets: AssetsJson) { + const { components: rawComponents, packages } = assets; + const libraryAsset = []; + const libraryMap = {}; + const packagesMap = {}; + packages.forEach(pkg => { + const { package: _package, library, urls, renderUrls, id } = pkg; + if (_package) { + libraryMap[id || _package] = library; + } + packagesMap[id || _package] = pkg; + if (renderUrls) { + libraryAsset.push(renderUrls); + } else if (urls) { + libraryAsset.push(urls); + } + }); + const assetLoader = new AssetLoader(); + await assetLoader.load(libraryAsset); + let newComponents = rawComponents; + if (rawComponents && rawComponents.length) { + const componentDescriptions: ComponentDescription[] = []; + const remoteComponentDescriptions: RemoteComponentDescription[] = []; + rawComponents.forEach((component: any) => { + if (!component) { + return; + } + if (component.exportName && component.url) { + remoteComponentDescriptions.push(component); + } else { + componentDescriptions.push(component); + } + }); + newComponents = [...componentDescriptions]; + + // 如果有远程组件描述协议,则自动加载并补充到资产包中,同时出发 designer.incrementalAssetsReady 通知组件面板更新数据 + if (remoteComponentDescriptions && remoteComponentDescriptions.length) { + await Promise.all( + remoteComponentDescriptions.map(async (component: any) => { + const { exportName, url, npm } = component; + await (new AssetLoader()).load(url); + function setAssetsComponent(component: any, extraNpmInfo: any = {}) { + const components = component.components; + if (Array.isArray(components)) { + components.forEach(d => { + newComponents = newComponents.concat({ + npm: { + ...npm, + ...extraNpmInfo, + }, + ...d, + } || []); + }); + return; + } + newComponents = newComponents.concat({ + npm: { + ...npm, + ...extraNpmInfo, + }, + ...component.components, + } || []); + } + + function setArrayAssets(value: any[], preExportName: string = '', preSubName: string = '') { + value.forEach((d: any, i: number) => { + const exportName = [preExportName, i.toString()].filter(d => !!d).join('.'); + const subName = [preSubName, i.toString()].filter(d => !!d).join('.'); + Array.isArray(d) ? setArrayAssets(d, exportName, subName) : setAssetsComponent(d, { + exportName, + subName, + }); + }); + } + if (window[exportName]) { + if (Array.isArray(window[exportName])) { + setArrayAssets(window[exportName] as any); + } else { + setAssetsComponent(window[exportName] as any); + } + } + return window[exportName]; + }), + ); + } + } + const lowcodeComponentsArray = []; + const proCodeComponentsMap = newComponents.reduce((acc, cur) => { + if ((cur.devMode || '').toLowerCase() === 'lowcode') { + lowcodeComponentsArray.push(cur); + } else { + acc[cur.componentName] = { + ...(cur.reference || cur.npm), + componentName: cur.componentName, + }; + } + return acc; + }, {}) + + function genLowCodeComponentsMap(components) { + const lowcodeComponentsMap = {}; + lowcodeComponentsArray.forEach((lowcode) => { + const id = lowcode.reference?.id; + const schema = packagesMap[id]?.schema; + const comp = genLowcodeComp(schema, {...components, ...lowcodeComponentsMap}); + lowcodeComponentsMap[lowcode.componentName] = comp; + }); + return lowcodeComponentsMap; + } + let components = await injectComponents(buildComponents(libraryMap, proCodeComponentsMap)); + const lowCodeComponents = genLowCodeComponentsMap(components); + return { + components: { ...components, ...lowCodeComponents } + } +} + +function genLowcodeComp(schema: ComponentSchema, components: any) { + return class LowcodeComp extends React.Component { + render(): React.ReactNode { + return createElement(ReactRenderer, { + ...this.props, + schema, + components, + designMode: '', + }); + } + }; +} +``` +## 联系我们 + + \ No newline at end of file diff --git a/docs/docs/guide/expand/editor/parts/partslcc.md b/docs/docs/guide/expand/editor/parts/partslcc.md new file mode 100644 index 0000000000..4d24b72f3a --- /dev/null +++ b/docs/docs/guide/expand/editor/parts/partslcc.md @@ -0,0 +1,92 @@ +--- +title: 低代码组件 +sidebar_position: 2 +--- +## 什么是低代码组件 +我们先了解一下什么是低代码组件,为什么要用低代码组件。 + +低代码组件是通过可视化的方式生产的组件,这些组件既可以用于低代码搭建体系,也可以用于 ProCode 开发体系(后续迭代)。 + +那么为什么我们要使用低代码的形式来开发组件: +* 首先轻快,低代码组件只需通过浏览器秒级完成初始化工作,不需要 ProCode 繁重的环境准备;环境一致(低代码环境),同时能够保证物料的开发环境和真实的运行环境是一致的,不会存在开发和运行环境不一致的问题。 +* 其次通用能力可视化方式抽象,提升研发效能,比如获取远程数据、视图开发、依赖管理、生命周期、事件绑定等功能。 + +低代码组件不是用来替代 ProCode 的开发方式,而是让开发者可以从 ProCode 中重复的工作脱离出来,抽象更多业务垂直的能力,从而起到提效的作用。 + +## 创建组件 + +环境准备:我们可以通过 Parts 提供的通用[低代码组件开发环境](https://parts.lowcode-engine.cn/material#/)开发。 + +点击开发新组件 --> 填写组件标题 --> 填写组件名称 --> 点击确定,完成组件创建工作。 + +![](https://img.alicdn.com/imgextra/i2/O1CN01OTQRew25y6WxuONIx_!!6000000007594-2-tps-3396-1696.png) + +## 组件开发 + +一张图速览低代码组件开发的功能模块,其中大部分功能可以参考[低代码引擎文档](https://lowcode-engine.cn/site/docs/guide/quickStart/intro)。 + +![](https://img.alicdn.com/imgextra/i1/O1CN01gx96E121qzv4smV2v_!!6000000007037-2-tps-3456-1930.png) + +### 依赖管理 + +依赖管理用于管理低代码组件本身的依赖(类似于 dependencies)。步骤:点击添加组件 --> 选择安装的组件 --> 保存并构建 (需要等待几分钟构建)。 + +![](https://img.alicdn.com/imgextra/i4/O1CN01wC9JPK1J9dKLca9wK_!!6000000000986-2-tps-1438-819.png) + +### 属性定义 + +用于定义组件接收外部传入的 propTypes,组件内部可以通过this.props.${属性名称}的方式获取属性值。 + +属性定义前建议先阅读 [物料描述详解](https://lowcode-engine.cn/site/docs/guide/expand/editor/metaSpec)、[预置设置器](https://lowcode-engine.cn/site/docs/guide/appendix/setters)。 + +![](https://img.alicdn.com/imgextra/i2/O1CN01wesIJA1nL1eSPrk7U_!!6000000005072-2-tps-1438-821.png) + +![](https://img.alicdn.com/imgextra/i3/O1CN01FZIRwv1es9lGplgIB_!!6000000003926-2-tps-1438-821.png) + +### 生命周期 + +低代码组件的开发支持 componentDidMount、componentDidUpdate、componentDidCatch、componentWillUnmount 几个生命周期 + +![](https://img.alicdn.com/imgextra/i4/O1CN010bnrxJ1oLlujlfFqj_!!6000000005209-2-tps-1438-819.png) + +### 组件调试 + +我们提供了一套线上实时调试的方案,只需点击右上角的调试按钮,就能自动创建一个低代码应用,在这个应用中可以实时调试当前的低代码组件。 + +![](https://img.alicdn.com/imgextra/i2/O1CN01Tk96vp1xrDeNeIUJD_!!6000000006496-2-tps-1438-820.png) + +在低代码应用中使用,组件面板 --> 低代码组件,找到对应的低代码组件拖入画布即可。 + +![](https://img.alicdn.com/imgextra/i2/O1CN01oGHLea1lzDAhZQQVO_!!6000000004889-2-tps-1438-819.png) + +### 组件发布 + +同时我们提供了组件发布的功能,用于组件版本管理,点击右上角的发布按钮即可发布组件 + +![](https://img.alicdn.com/imgextra/i2/O1CN017suVAD1NXEC8zQgO1_!!6000000001579-2-tps-1438-821.png) + +## 组件使用 + +组件的消费是通过资产包来管理的,详情请参考 [资产包管理](./partsassets)。 + +## 组件导出 + +开发好的低代码组件可以导出成为 React 组件,脱离低代码引擎独立使用。同时导出功能也为您的组件留出一份备份,您可以放心使用本产品的服务,而不用担心万一出现的不能服务的场景。 + +在物料列表页面,低代码组件会有一个导出的动作。 + +![](https://img.alicdn.com/imgextra/i2/O1CN016oUByO21spVHZvvw2_!!6000000007041-2-tps-1395-413.png) + +点击导出后,就会开启导出低代码组件的过程。这个过程持续 10s+,导出完成后会为您自动下载对应的 zip 包。 + +![](https://img.alicdn.com/imgextra/i1/O1CN01lctpIo1aDcEvu75Mo_!!6000000003296-2-tps-1399-512.png) + +zip 包解压后可以看到一个完整的组件脚手架工程,您可以在这个工程里继续开发调试,或者发布到合适的 npm 源中。 + +![](https://img.alicdn.com/imgextra/i1/O1CN010aAjsf1xYRPZBAh7d_!!6000000006455-2-tps-2154-1072.png) + +注意:目前导出功能暂不支持 低代码组件嵌套。 + +## 联系我们 + + \ No newline at end of file diff --git a/docs/docs/guide/expand/editor/parts/prototype.md b/docs/docs/guide/expand/editor/parts/prototype.md new file mode 100644 index 0000000000..b90728f657 --- /dev/null +++ b/docs/docs/guide/expand/editor/parts/prototype.md @@ -0,0 +1,121 @@ +--- +title: React 组件导入 +sidebar_position: 3 +--- +## 介绍 +大家在使用[低代码引擎](https://lowcode-engine.cn/)构建低代码应用平台时,遇到的一个主要问题是如何让已有的 React 组件能够快速低成本地接入进来。这个问题拆解下来主要包括两个子问题: +1. 如何给已有组件[配置物料描述](/site/docs/specs/material-spec), +2. 如何构建出一个低代码引擎能够识别的资产包 (Assets)。 + +我们的产品「[Parts·造物](https://parts.lowcode-engine.cn/)」可以帮助大家解决这个问题。我们通过在线可视化的方式完成物料描述配置,并且提供一键打包的功能生成引擎可以识别的资产包。 + +## 导入物料 +首先,我们需要在 [物料管理](/site/docs/specs/material-spec) 页面导入我们需要进行在线物料描述配置的物料。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01IyZdZf1L1VWWU3dnp_!!6000000001239-2-tps-1399-342.png) + +- 点击列表左上方的 导入已有物料 按钮 +- 在弹框中输入 npm 包名 +- 点击 获取包信息 按钮,获取 npm 包基本信息 +- 点击确定,导入成功 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN019FwWgs1kqgAXq5UNJ_!!6000000004735-2-tps-640-315.png) +## 配置管理 +第二步:物料导入以后,我们就可以为导入的物料新增[物料描述配置](/site/docs/specs/material-spec),点击右侧的组件配置开始配置。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01kqymdB1nkDQclPk7F_!!6000000005127-2-tps-965-261.png) +### 新增配置 + +- 点击配置管理右上角的 新增配置 + - 选择组件的版本号 + - 填写组件路径,一般和 npm 包的 package.json 里的 main 字段相同(如果填写错误,后面会渲染不出来) + - 描述字段用于给这份配置增加一些备注信息。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01i78OhT1cKbVWnXRNu_!!6000000003582-2-tps-596-418.png) + +为了降低配置成本,第一次新增配置的时候会自动解析组件代码,生成一份初始化组件物料描述。所以需要等待片刻,用于代码解析。解析完成后,点击配置按钮即可进入在线配置界面。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01R24mTl1tJY3oJ5DCi_!!6000000005881-2-tps-963-232.png) + +### 组件描述配置 +操作界面如下,接下来讲具体的配置流程 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01XjSW9I1u662raRg8E_!!6000000005987-2-tps-1438-938.png) + +#### 新增组件 + +如果新增配置的过程中,代码自动解析失败或者解析出来的组件列表不满足开发要求,我们可以点击左侧组件列表插件 新增 按钮,添加新的组件,具体的字段描述可以参考提示内容,以 [react-color](https://github.com/casesandberg/react-color) 为例: + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01A9VFfQ1m9kH2Qliz4_!!6000000004912-2-tps-1436-1005.png) + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01klci7y1IUPflKpeVB_!!6000000000896-2-tps-1193-704.png) +#### 给组件增加物料描述 + +- 打开左侧 Setter 面板 +- 按照组件的属性拖入需要 Setter 类型(如图中组件的 width 属性,拖入数字 Setter) +- 各种 Setter 的介绍可以参看这篇文档:[预置设置器列表](/site/docs/guide/appendix/setters) +- 配置属性的基本信息(如图所示) +- 配置完成后点击右上角的保存 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01gxLKBp1RaDEMPS54O_!!6000000002127-2-tps-1434-967.png) + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01uReCQ825yYuwIfj2J_!!6000000007595-2-tps-925-360.png) + +#### 高级配置(属性联动) + +举个栗子:如图所示,如果期望“设置器”这个配置项的值“被修改”的时候,下面的“默认值”跟着变化。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01bg7X571bpSZdnXTBW_!!6000000003514-2-tps-371-572.png) + +如何使用 + +组件的属性配置目前支持 3 个基本的联动函数: + +- 显示状态:返回 true | false,如果返回 true,表示组件配置显示,否则配置时不显示 +- 获取值:当调用该配置节点的 getValue 方法时触发的方法 +- 值变化:当调用该配置节点的 setValue 方法时触发的方法 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN018ZJAJO21q57TdWfjM_!!6000000007035-2-tps-316-142.png) + +方法的第一个参数都是当前配置节点的对象,常用到的有以下几个: + +- getValue(): 获取当前节点的值,如果当前节点是子节点的话,否则为 undefined +- setValue(): 设置当前节点的值,如果当前节点是子节点的话 +- parent: 当前节点的父节点 +- getPropValue(propName): 父节点获取子节点的属性值,propName 为子节点的属性名称 +- setPropValue(propName, value): 父节点设置子节点的属性值,propName 为子节点的属性名称,value 为设置的值 +- getConfig: 获取当前节点的配置,如 title、setter 等 + + +#### 调试物料描述 + +点击右上角的预览按钮,开始调试我们刚刚配置的属性,如果是组件的首次预览,会有一段组件构建的过程(构建出 umd 包的过程),构建完成后就可以调试我们的配置了。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN012biqEn1uGAl650nb2_!!6000000006009-2-tps-1431-373.png) + +#### 发布物料描述 +物料描述调试没问题后,就可以到项目中去使用了,使用前需要先发布物料描述 + +- 点击右上角的发布按钮 +- 选择需要发布的组件 +- 点击确定发布完成 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01uwa8RH1QDwM7FN31k_!!6000000001943-2-tps-1431-734.png) +## 资产包 +第三步:物料描述发布完成后,接下来我们就需要构建出可用的资产包用于低代码应用中。 + +#### 资产包构建 +有两种方式可以构建资产包: +- 一种是通过 [`我的资产包`] 资产包管理模块进行整个资产包生命周期的管理,当然也包括资产包的构建,可参考 [资产包管理](./partsassets) +- 一种是通过 [`我的物料`] 组件物料管理模块的 `资产包构建` 进行构建, 具体操作如下: + + - 选择需要构建的组件 + - 点击构建资产包按钮 + - 选择刚刚的物料描述配置 + - 开始构建,构建完成后你将得到一份 json 文件(里面包含了物料描述和 umd 包),就可以到项目中使用了 + +#### 资产包使用 +详情请参考 [资产包管理](./partsassets#使用资产包) + +## 联系我们 + + diff --git a/docs/docs/guide/expand/editor/pluginContextMenu.md b/docs/docs/guide/expand/editor/pluginContextMenu.md new file mode 100644 index 0000000000..962c913e7e --- /dev/null +++ b/docs/docs/guide/expand/editor/pluginContextMenu.md @@ -0,0 +1,82 @@ +--- +title: 插件扩展 - 编排扩展 +sidebar_position: 6 +--- + +## 场景一:扩展选中节点操作项 + +### 增加节点操作项 +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01J7PrJc1S86XNDBIFQ_!!6000000002201-2-tps-1240-292.png) + +选中节点后,在选中框的右上角有操作按钮,编排模块默认实现了查看组件直系父节点、复制节点和删除节点按钮外,还可以通过相关 API 来扩展更多操作,如下代码: + +```typescript +import { plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext, IPublicModelNode } from '@alilc/lowcode-types'; +import { Icon, Message } from '@alifd/next'; + +const addHelloAction = (ctx: IPublicModelPluginContext) => { + return { + async init() { + ctx.material.addBuiltinComponentAction({ + name: 'hello', + content: { + icon: , + title: 'hello', + action(node: IPublicModelNode) { + Message.show('Welcome to Low-Code engine'); + }, + }, + condition: (node: IPublicModelNode) => { + return node.componentMeta.componentName === 'NextTable'; + }, + important: true, + }); + }, + }; +}; +addHelloAction.pluginName = 'addHelloAction'; +await plugins.register(addHelloAction); +``` + +**_效果如下:_** + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01O8W2H61ybw2b7K5nV_!!6000000006598-2-tps-1315-343.png) + +具体 API 参考:[API 文档](/site/docs/api/material#addbuiltincomponentaction) +### 删除节点操作项 + +```typescript +import { plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +const removeCopyAction = (ctx: IPublicModelPluginContext) => { + return { + async init() { + ctx.material.removeBuiltinComponentAction('copy'); + } + } +}; +removeCopyAction.pluginName = 'removeCopyAction'; +await plugins.register(removeCopyAction); +``` + +**_效果如下:_** + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01Gfnu8J1O7PTRdoFQZ_!!6000000001658-2-tps-1319-290.png) + +具体 API 参考:[API 文档](/site/docs/api/material#removebuiltincomponentaction) + +## 实际案例 + +### 区块管理 + +- 仓库地址:[https://github.com/alibaba/lowcode-plugins](https://github.com/alibaba/lowcode-plugins) +- 具体代码:[https://github.com/alibaba/lowcode-plugins/tree/main/packages/action-block](https://github.com/alibaba/lowcode-plugins/tree/main/packages/action-block) +- 直播回放: + - [低代码引擎项目实战 (9)-区块管理 (1)-保存为区块](https://www.bilibili.com/video/BV1YF411M7RK/) + - [低代码引擎项目实战 (10)-区块管理 - 区块面板](https://www.bilibili.com/video/BV1FB4y1S7tu/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - ICON 优化](https://www.bilibili.com/video/BV1zr4y1H7Km/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - 自动截图](https://www.bilibili.com/video/BV1GZ4y117VH/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - 样式优化](https://www.bilibili.com/video/BV1Pi4y1S7ZT/) + - [阿里低代码引擎项目实战 (12)-区块管理 (完结)-给引擎插件提个 PR](https://www.bilibili.com/video/BV1hB4y1277o/) diff --git a/docs/docs/guide/expand/editor/pluginWidget.md b/docs/docs/guide/expand/editor/pluginWidget.md new file mode 100644 index 0000000000..06125575f6 --- /dev/null +++ b/docs/docs/guide/expand/editor/pluginWidget.md @@ -0,0 +1,214 @@ +--- +title: 插件扩展 - 面板扩展 +sidebar_position: 5 +--- + +## 插件简述 + +插件功能赋予低代码引擎更高的灵活性,低代码引擎的生态提供了一些官方的插件,但是无法满足所有人的需求,所以提供了强大的插件定制功能。 + +通过定制插件,在和低代码引擎解耦的基础上,我们可以和引擎核心模块进行交互,从而满足多样化的功能。不仅可以自定义插件的 UI,还可以实现一些非 UI 的逻辑: + +1. 调用编辑器框架提供的 API 进行编辑器操作或者 schema 操作; +2. 通过插件类的生命周期函数实现一些插件初始化的逻辑; +3. 通过实现监听编辑器内的消息实现特定的切片逻辑(例如面板打开、面板关闭等); + +> 本文仅介绍面板层面的扩展,编辑器插件层面的扩展可以参考 ["插件扩展 - 编排扩展"](./pluginContextMenu.md) 章节。 + +## 注册插件 API + +```typescript +import { plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +const pluginA = (ctx: IPublicModelPluginContext, options: any) => { + return { + init() { + console.log(options.key); + // 往引擎增加面板 + ctx.skeleton.add({ + // area 配置见下方说明 + area: 'leftArea', + // type 配置见下方说明 + type: 'PanelDock', + content:
demo
, + }); + ctx.logger.log('打个日志'); + }, + destroy() { + console.log('我被销毁了~'); + }, + }; +}; + +pluginA.pluginName = 'pluginA'; + +plugins.register(pluginA, { key: 'test' }); +``` + +> 如果您想了解抽取出来的插件如何封装成为一个 npm 包并提供给社区,可以参考[“低代码生态脚手架 & 调试机制”](./cli)章节。 + +## 面板插件配置说明 + +面板插件是作用于设计器的,主要是通过按钮、图标等展示在设计器的骨架中。设计器的骨架我们分为下面的几个区域,而我们的插件大多数都是作用于这几个区域的。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01Bkfm9E1MQWmBWeIOh_!!6000000001429-2-tps-1920-1080.png) + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01y05ZHC1Gix0p4nXxH_!!6000000000657-2-tps-3068-1648.png) + +### 展示区域 area + +#### topArea + +展示在设计器的顶部区域,常见的相关区域的插件主要是:、 + +1. 注册设计器 Logo; +2. 设计器操作回退和撤销按钮; +3. 全局操作按钮,例如:保存、预览等; + +#### leftArea + +左侧区域的展示形式大多数是 Icon 和对应的面板,通过点击 Icon 可以展示对应的面板并隐藏其他的面板。 + +该区域相关插件的主要有: + +1. 大纲树展示,展示该设计器设计页面的大纲。 +2. 组件库,展示注册到设计器中的组件,点击之后,可以从组件库面板中拖拽到设计器的画布中。 +3. 数据源面板 +4. JS 等代码面板。 + +可以发现,这个区域的面板大多数操作时是不需要同时并存的,且交互比较复杂的,需要一个更整块的区域来进行操作。 + +#### centerArea + +画布区域,由于画布大多数是展示作用,所以一般扩展的种类比较少。常见的扩展有: + +1. 画布大小修改 +2. 物料选中扩展区域修改 + +#### rightArea + +右侧区域,常用于组件的配置。常见的扩展有:统一处理组件的配置项,例如统一删除某一个配置项,统一添加某一个配置项的。 + +#### toolbar + +跟 topArea 类似,按需放置面板插件~ + +### 展示形式 type + +#### PanelDock + +PanelDock 是以面板的形式展示在设计器的左侧区域的。其中主要有两个部分组成,一个是图标,一个是面板。当点击图标时可以控制面板的显示和隐藏。 + +下图是组件库插件的展示效果。 + +![Feb-08-2022 19-44-15.gif](https://img.alicdn.com/imgextra/i3/O1CN01XCrv5Q1hR5BgsyAiq_!!6000000004273-1-tps-1536-790.gif) + +其中右上角可以进行固定,可以对弹出的宽度做设定 + +接入可以参考代码 + +```javascript +import { skeleton } from '@alilc/lowcode-engine'; + +skeleton.add({ + area: 'leftArea', // 插件区域 + type: 'PanelDock', // 插件类型,弹出面板 + name: 'sourceEditor', + content: SourceEditor, // 插件组件实例 + props: { + align: "left", + icon: "wenjian", + description: "JS 面板", + }, + panelProps: { + floatable: true, // 是否可浮动 + height: 300, + hideTitleBar: false, + maxHeight: 800, + maxWidth: 1200, + title: "JS 面板", + width: 600, + }, +}); +``` + +#### Widget + +Widget 形式是直接渲染在当前编辑器的对应位置上。如 demo 中在设计器顶部的所有组件都是这种展现形式。 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01h89p5W1pfknnzwMqS_!!6000000005388-2-tps-1988-94.png) + +接入可以参考代码: + +```javascript +import { skeleton } from '@alilc/lowcode-engine'; +// 注册 logo 面板 +skeleton.add({ + area: 'topArea', + type: 'Widget', + name: 'logo', + content: Logo, // Widget 组件实例 + contentProps: { // Widget 插件 props + logo: + "https://img.alicdn.com/tfs/TB1_SocGkT2gK0jSZFkXXcIQFXa-66-66.png", + href: "/", + }, + props: { + align: 'left', + width: 100, + }, +}); +``` + +#### Dock + +一个图标的表现形式,可以用于语言切换、跳转到外部链接、打开一个 widget 等场景。 + +```javascript +import { skeleton } from '@alilc/lowcode-engine'; + +skeleton.add({ + area: 'leftArea', + type: 'Dock', + name: 'opener', + props: { + icon: Icon, // Icon 组件实例 + align: 'bottom', + onClick: function () { + // 打开外部链接 + window.open('https://lowcode-engine.cn'); + // 显示 widget + skeleton.showWidget('xxx'); + } + } +}); +``` + +#### Panel + +一般不建议单独使用,通过 PanelDock 使用~ + +## 实际案例 + +### 页面管理面板 + +- 仓库地址:[https://github.com/mark-ck/lowcode-portal](https://github.com/mark-ck/lowcode-portal) +- 具体代码:[https://github.com/mark-ck/lowcode-portal/blob/master/src/plugins/pages-plugin/index.tsx](https://github.com/mark-ck/lowcode-portal/blob/master/src/plugins/pages-plugin/index.tsx) +- 直播回放: + - [低代码引擎项目实战 (4)-自定义插件 - 页面管理](https://www.bilibili.com/video/BV17a411i73f/) + - [低代码引擎项目实战 (4)-自定义插件 - 页面管理 - 后端](https://www.bilibili.com/video/BV1uZ4y1U7Ly/) + - [低代码引擎项目实战 (4)-自定义插件 - 页面管理 - 前端](https://www.bilibili.com/video/BV1Yq4y1a74P/) + - [低代码引擎项目实战 (4)-自定义插件 - 页面管理 - 完结](https://www.bilibili.com/video/BV13Y4y1e7EV/) + +### 区块面板 + +- 仓库地址:[https://github.com/alibaba/lowcode-plugins](https://github.com/alibaba/lowcode-plugins) +- 具体代码:[https://github.com/alibaba/lowcode-plugins/tree/main/packages/plugin-block](https://github.com/alibaba/lowcode-plugins/tree/main/packages/plugin-block) +- 直播回放: + - [低代码引擎项目实战 (9)-区块管理 (1)-保存为区块](https://www.bilibili.com/video/BV1YF411M7RK/) + - [低代码引擎项目实战 (10)-区块管理 - 区块面板](https://www.bilibili.com/video/BV1FB4y1S7tu/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - ICON 优化](https://www.bilibili.com/video/BV1zr4y1H7Km/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - 自动截图](https://www.bilibili.com/video/BV1GZ4y117VH/) + - [阿里巴巴低代码引擎项目实战 (11)-区块管理 - 样式优化](https://www.bilibili.com/video/BV1Pi4y1S7ZT/) + - [阿里低代码引擎项目实战 (12)-区块管理 (完结)-给引擎插件提个 PR](https://www.bilibili.com/video/BV1hB4y1277o/) diff --git a/docs/docs/guide/expand/editor/setter.md b/docs/docs/guide/expand/editor/setter.md new file mode 100644 index 0000000000..4f0e0219fc --- /dev/null +++ b/docs/docs/guide/expand/editor/setter.md @@ -0,0 +1,241 @@ +--- +title: 设置器扩展 +sidebar_position: 7 +--- +## 设置器简述 + +设置器主要用于低代码组件属性值的设置,顾名思义叫"设置器",又称为 Setter。由于组件的属性有各种类型,需要有与之对应的设置器支持,每一个设置器对应一个值的类型。 + +### 设计器展示位置 + +设置器展示在编辑器的右边区域,如下图: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01F0yBV91jNzkZKLzvJ_!!6000000004537-2-tps-3836-1730.png) + +其中包含四类设置器: + +- 属性:展示该物料常规的属性 +- 样式:展示该物料样式的属性 +- 事件:如果该物料有声明事件,则会出现事件面板,用于绑定事件。 +- 高级:两个逻辑相关的属性,**条件渲染**和**循环** + +### 设置器类型 + +上述区域中是有多项设置器的,对于一个组件来说,每一项配置都对应一个设置器,比如我们的配置是一个文本,我们需要的是文本设置器,我们需要配置的是数字,我们需要的就是数字设置器。 +下图中的标题和按钮类型配置就分别是文本设置器和下拉框设置器。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01uMd1zQ20fiXawR4IU_!!6000000006877-2-tps-2120-1460.png) + +我们提供了常用的设置器作为内置设置器,也提供了定制能力帮助大家开发特定需求的设置器。 + +## 为物料配置设置器 + +我们提供了[常用的设置器](/site/docs/guide/appendix/setters)作为内置设置器。 + +我们可以将目标组件的属性值类型值配置到物料资源配置文件中: + +```json +{ + "componentName": "Message", + "title": "Message", + "configure": { + "props": [ + { + "name": "type", + "setter": "InputSetter" + } + ] + } +} +``` + +props 字段是入料模块扫描自动填入的类型,用户可以通过 configure 节点进行配置通过 override 节点对属性的声明重新定义,setter 就是注册在引擎中的 setter。 + +为物料配置引擎内置的 setter 时,均可以使用对应 setter 的高级功能,对应功能参考“全部内置设置器”章节下的对应 setter 文章。 + +### 对高级功能的配置如下: + +例如我们需要在 NumberSetter 中配置 units 属性,可以在 asset.json 中声明。 + +```json +"configure": { + "component": { + "isContainer": true, + "nestingRule": { + "parentWhitelist": [ + "NextP" + ] + } + }, + "props": [ + { + "name": "width", + "title": "宽度", + "initialValue": "auto", + "defaultValue": "auto", + "condition": { + "type": "JSFunction", + "value": "() => false" + }, + "setter": { + "componentName": "NumberSetter", + "props": { + "units": [ + { + "type": "px", + "list": true + }, + { + "type": "%", + "list": true + } + ] + } + } + }, + ], + "supports": { + "style": true + } +}, +``` + +## 自定义设置器 +### 编写 AltStringSetter + +我们编写一个简单的 Setter,它的功能如下: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fQ4GLd1RzrPSdULiw_!!6000000002183-2-tps-720-90.png) + +**代码如下:** +```tsx +import * as React from "react"; +import { Input } from "@alifd/next"; +import "./index.scss"; + +interface AltStringSetterProps { + // 当前值 + value: string; + // 默认值 + defaultValue: string; + // setter 唯一输出 + onChange: (val: string) => void; + // AltStringSetter 特殊配置 + placeholder: string; +} + +export default class AltStringSetter extends React.PureComponent { + // 声明 Setter 的 title + static displayName = 'AltStringSetter'; + + componentDidMount() { + const { onChange, value, defaultValue } = this.props; + if (value == undefined && defaultValue) { + onChange(defaultValue); + } + } + + render() { + const { onChange, value, placeholder } = this.props; + return ( + onChange(val)} + > + ); + } +} +``` + +#### setter 和 setter/plugin 之间的联动 + +我们采用 emit 来进行相互之前的通信,首先我们在 A setter 中进行事件注册: + +```javascript +import { event } from '@alilc/lowcode-engine'; + +componentDidMount() { + // 这里由于面板上会有多个 setter,这里我用 field.id 来标记 setter 名 + this.emitEventName = `${SETTER_NAME}-${this.props.field.id}`; + event.on(`${this.emitEventName}.bindEvent`, this.bindEvent); +} + +bindEvent = (eventName) => { + // do someting +} + +componentWillUnmount() { + // setter 是以实例为单位的,每个 setter 注销的时候需要把事件也注销掉,避免事件池过多 + event.off(`${this.emitEventName}.bindEvent`, this.bindEvent); +} +``` + +在 B setter 中触发事件,来完成通信: + +```javascript +import { event } from '@alilc/lowcode-engine'; + +bindFunction = () => { + const { field, value } = this.props; + // 这里展示的和插件进行通信,事件规则是插件名 + 方法 + event.emit('eventBindDialog.openDialog', field.name, this.emitEventName); +} +``` + +#### 修改同级 props 的其他属性值 + +setter 本身只影响其中一个 props 的值,如果需要影响其他组件的 props 的值,需要使用 field 的 props: + +```javascript +bindFunction = () => { + const { field, value } = this.props; + const propsField = field.parent; + // 获取同级其他属性 showJump 的值 + const otherValue = propsField.getPropValue('showJump'); + // set 同级其他属性 showJump 的值 + propsField.setPropValue('showJump', false); +} +``` + +### 注册 AltStringSetter + +我们需要在低代码引擎中注册 Setter,这样就可以通过 AltStringSetter 的名字在物料中使用了。 + +```typescript +import AltStringSetter from './AltStringSetter'; +const registerSetter = window.AliLowCodeEngine.setters.registerSetter; +registerSetter('AltStringSetter', AltStringSetter); +``` + +### 物料中使用 + +我们需要将目标组件的属性值类型值配置到物料资源配置文件中,其中核心配置如下: + +```json +{ + "props": [ + { + "name": "type", + "setter": "AltStringSetter" + } + ] +} +``` + +在物料中的相关配置如下: + +```json +{ + "componentName": "Message", + "title": "Message", + "configure": { + "props": [ + { + "name": "type", + "setter": "AltStringSetter" + } + ] + } +} +``` \ No newline at end of file diff --git a/docs/docs/guide/expand/editor/summary.md b/docs/docs/guide/expand/editor/summary.md new file mode 100644 index 0000000000..814340f3d3 --- /dev/null +++ b/docs/docs/guide/expand/editor/summary.md @@ -0,0 +1,92 @@ +--- +title: 编辑态扩展简述 +sidebar_position: 0 +--- +## 扩展点简述 + +我们可以从 Demo 的项目中看到页面中有很多的区块: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01WkdvNi1TamxZblYFA_!!6000000002399-2-tps-3840-2160.png) +这些功能点背后都是可扩展项目,如下图所示: +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01wZLOzm24hmnMTwXdF_!!6000000007423-2-tps-3838-1914.png) + +- 插件定制:可以配置低代码编辑器的功能和面板 +- 物料定制:可以配置能够拖入的物料 +- 操作辅助区定制:可以配置编辑器画布中的操作辅助区功能 +- 设置器定制:可以配置编辑器中组件的配置表单 + +我们从可扩展项目的视角,可以把低代码引擎架构理解为下图: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01fhZ3Q11hwE7RwSq7g_!!6000000004341-2-tps-3840-2160.png) +(注:引擎内核中大量数据交互的细节被简化,这张图仅仅强调编辑器和外部扩展的交互) + +## 配置扩展点 + +### 配置物料 +通过配置注入物料,这里的配置是物料中心根据物料资产包协议生成的,后面“物料扩展”章节会有详细说明。 +```typescript +import { material } from '@alilc/lowcode-engine'; +// 假设您已把物料配置在本地 +import assets from './assets.json'; + +// 静态加载 assets +material.setAssets(assets); +``` + +也可以通过异步加载物料中心上的物料。 +```typescript +import { material, plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +// 动态加载 assets +plugins.register((ctx: IPublicModelPluginContext) => { + return { + name: 'ext-assets', + async init() { + try { + // 将下述链接替换为您的物料即可。无论是通过 utils 从物料中心引入,还是通过其他途径如直接引入物料描述 + const res = await window.fetch('https://fusion.alicdn.com/assets/default@0.1.95/assets.json') + const assets = await res.text() + material.setAssets(assets) + } catch (err) { + console.error(err) + } + }, + } +}).catch(err => console.error(err)); +``` + +### 配置插件 +可以通过 npm 包的方式引入社区插件,配置如下所示: +```typescript +import { plugins } from '@alilc/lowcode-engine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; +import PluginIssueTracker from '@alilc/lowcode-plugin-issue-tracker'; + +// 注册一个提 issue 组件到您的编辑器中,方位默认在左栏下侧 +plugins.register(PluginIssueTracker) + .catch(err => console.error(err)); +``` +后续“插件扩展”章节会详细说明。 + +### 配置设置器 +低代码引擎默认内置了设置器(详见“配置设置器”章节)。您可以通过 npm 包的方式引入自定义的设置器,配置如下所示: +```typescript +import { setters } from '@alilc/lowcode-engine'; +// 假设您自定义了一个 setter +import MuxMonacoEditorSetter from './components/setters/MuxMonacoEditorSetter'; + +// 注册设置器 +setters.registerSetter({ + MuxMonacoEditorSetter: { + component: MuxMonacoEditorSetter, + title: 'Textarea', + condition: (field) => { + const v = field.getValue() + return typeof v === 'string' + }, + }, +}); +``` +后续“设置器扩展”章节会详细说明。 + +> 本章节所有可扩展配置内容在 demo 中均可找到:[https://github.com/alibaba/lowcode-demo/tree/main/demo-general](https://github.com/alibaba/lowcode-demo/tree/main/demo-general) diff --git a/docs/docs/guide/expand/editor/theme.md b/docs/docs/guide/expand/editor/theme.md new file mode 100644 index 0000000000..897b6b360b --- /dev/null +++ b/docs/docs/guide/expand/editor/theme.md @@ -0,0 +1,157 @@ +--- +title: 主题色扩展 +sidebar_position: 9 +--- + +## 简介 + +主题色扩展允许用户定制多样的设计器主题,增加界面的个性化和品牌识别度。 + +## 设计器主题色定制 + +在 CSS 的根级别定义主题色变量可以确保这些变量在整个应用中都可用。例如: + +```css +:root { + --color-brand: rgba(0, 108, 255, 1); /* 主品牌色 */ + --color-brand-light: rgba(25, 122, 255, 1); /* 浅色品牌色 */ + --color-brand-dark: rgba(0, 96, 229, 1); /* 深色品牌色 */ +} + +``` + +将样式文件引入到你的设计器中,定义的 CSS 变量就可以改变设计器的主题色了。 + +### 主题色变量 + +以下是低代码引擎设计器支持的主题色变量列表,以及它们的用途说明: + +#### 品牌相关颜色 + +- `--color-brand`: 主品牌色 +- `--color-brand-light`: 浅色品牌色 +- `--color-brand-dark`: 深色品牌色 + +#### Icon 相关颜色 + +- `--color-icon-normal`: 默认状态 +- `--color-icon-light`: icon light 状态 +- `--color-icon-hover`: 鼠标悬停状态 +- `--color-icon-active`: 激活状态 +- `--color-icon-reverse`: 反色状态 +- `--color-icon-disabled`: 禁用状态 +- `--color-icon-pane`: 面板颜色 + +#### 线条和文本颜色 + +- `--color-line-normal`: 线条颜色 +- `--color-line-darken`: 线条颜色(darken) +- `--color-title`: 标题颜色 +- `--color-text`: 文字颜色 +- `--color-text-dark`: 文字颜色(dark) +- `--color-text-light`: 文字颜色(light) +- `--color-text-reverse`: 反色情况下,文字颜色 +- `--color-text-disabled`: 禁用态文字颜色 + +#### 菜单颜色 +- `--color-context-menu-text`: 菜单项颜色 +- `--color-context-menu-text-hover`: 菜单项 hover 颜色 +- `--color-context-menu-text-disabled`: 菜单项 disabled 颜色 + +#### 字段和边框颜色 + +- `--color-field-label`: field 标签颜色 +- `--color-field-text`: field 文本颜色 +- `--color-field-placeholder`: field placeholder 颜色 +- `--color-field-border`: field 边框颜色 +- `--color-field-border-hover`: hover 态下,field 边框颜色 +- `--color-field-border-active`: active 态下,field 边框颜色 +- `--color-field-background`: field 背景色 + +#### 状态颜色 + +- `--color-success`: success 颜色 +- `--colo-success-dark`: success 颜色(dark) +- `--color-success-light`: success 颜色(light) +- `--color-warning`: warning 颜色 +- `--color-warning-dark`: warning 颜色(dark) +- `--color-warning-light`: warning 颜色(light) +- `--color-information`: information 颜色 +- `--color-information-dark`: information 颜色(dark) +- `--color-information-light`: information 颜色(light) +- `--color-error`: error 颜色 +- `--color-error-dark`: error 颜色(dark) +- `--color-error-light`: error 颜色(light) +- `--color-purple`: purple 颜色 +- `--color-brown`: brown 颜色 + +#### 区块背景色 + +- `--color-block-background-normal`: 区块背景色 +- `--color-block-background-light`: 区块背景色(light)。 +- `--color-block-background-shallow`: 区块背景色 shallow +- `--color-block-background-dark`: 区块背景色(dark) +- `--color-block-background-disabled`: 区块背景色(disabled) +- `--color-block-background-active`: 区块背景色(active) +- `--color-block-background-active-light`: 区块背景色(active light) +- `--color-block-background-warning`: 区块背景色(warning) +- `--color-block-background-error`: 区块背景色(error) +- `--color-block-background-success`: 区块背景色(success) +- `--color-block-background-deep-dark`: 区块背景色(deep-dark),作用于多个组件同时拖拽的背景色。 + +#### 引擎相关颜色 + +- `--color-canvas-detecting-background`: 画布组件 hover 时遮罩背景色。 + +#### 其他区域背景色 + +- `--color-layer-mask-background`: 拖拽元素时,元素原来位置的遮罩背景色 +- `--color-layer-tooltip-background`: tooltip 背景色 +- `--color-pane-background`: 面板背景色 +- `--color-background`: 设计器主要背景色 +- `--color-top-area-background`: topArea 背景色,优先级大于 `--color-pane-background` +- `--color-left-area-background`: leftArea 背景色,优先级大于 `--color-pane-background` +- `--color-toolbar-background`: toolbar 背景色,优先级大于 `--color-pane-background` +- `--color-workspace-left-area-background`: 应用级 leftArea 背景色,优先级大于 `--color-pane-background` +- `--color-workspace-top-area-background`: 应用级 topArea 背景色,优先级大于 `--color-pane-background` +- `--color-workspace-sub-top-area-background`: 应用级二级 topArea 背景色,优先级大于 `--color-pane-background` + +#### 其他变量 + +- `--workspace-sub-top-area-height`: 应用级二级 topArea 高度 +- `--top-area-height`: 顶部区域的高度 +- `--workspace-sub-top-area-margin`: 应用级二级 topArea margin +- `--workspace-sub-top-area-padding`: 应用级二级 topArea padding +- `--workspace-left-area-width`: 应用级 leftArea width +- `--left-area-width`: leftArea width +- `--simulator-top-distance`: simulator 距离容器顶部的距离 +- `--simulator-bottom-distance`: simulator 距离容器底部的距离 +- `--simulator-left-distance`: simulator 距离容器左边的距离 +- `--simulator-right-distance`: simulator 距离容器右边的距离 +- `--toolbar-padding`: toolbar 的 padding +- `--toolbar-height`: toolbar 的高度 +- `--pane-title-height`: 面板标题高度 +- `--pane-title-font-size`: 面板标题字体大小 +- `--pane-title-padding`: 面板标题边距 +- `--context-menu-item-height`: 右键菜单项高度 + + + +### 低代码引擎生态主题色定制 + +插件、物料、设置器等生态为了支持主题色需要对样式进行改造,需要对生态中的样式升级为 css 变量。例如: + +```css +/* before */ +background: #006cff; + +/* after */ +background: var(--color-brand, #006cff); + +``` + +这里 `var(--color-brand, #默认色)` 表示使用 `--color-brand` 变量,如果该变量未定义,则使用默认颜色(#默认色)。 + +### fusion 物料进行主题色扩展 + +如果使用了 fusion 组件时,可以通过 [fusion 平台](https://fusion.design/) 进行主题色定制。在平台上,您可以选择不同的主题颜色,并直接应用于您的 fusion 组件,这样可以无缝地集成到您的应用设计中。 \ No newline at end of file diff --git a/docs/docs/guide/expand/runtime/_category_.json b/docs/docs/guide/expand/runtime/_category_.json new file mode 100644 index 0000000000..f382ad4068 --- /dev/null +++ b/docs/docs/guide/expand/runtime/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "扩展运行时", + "position": 2, + "collapsed": false, + "collapsible": true +} diff --git a/docs/docs/guide/expand/runtime/codeGeneration.md b/docs/docs/guide/expand/runtime/codeGeneration.md new file mode 100644 index 0000000000..71cf81bd1c --- /dev/null +++ b/docs/docs/guide/expand/runtime/codeGeneration.md @@ -0,0 +1,133 @@ +--- +title: 使用出码功能 +sidebar_position: 1 +--- + +## 出码简述 +所谓出码,即将低代码编排出的 schema 进行解析并转换成最终可执行的代码的过程。 +## 出码的适用范围 +出码是为了更高效的运行和更灵活地定制渲染,相对而言,基于 Schema 的运行时渲染,有着能实时响应内容的变化和接入成本低的优点,但是也存在着实时解析运行的性能开销比较大和包大小比较大的问题,而且无法自由地进行扩展二次开发,功能自由度受到一定程度限制。 +当然,出码也会存在一些限制:一方面需要额外的接入成本,另一方面通常需要额外的生成代码和打包构建的时间,难以做到基于 Schema 的运行时渲染那样保存即预览的效果。 + +所以不是所有场景都建议做出码,一般来说以下 3 个场景可以考虑使用出码进行优化。 + +### 场景一:想要极致的打开速度,降低 LCP/FID +这种场景比较常见的是 C 端应用,比如手淘上的页面和手机钉钉上的页面,要求能够尽快得响应用户操作,不要出现卡死的情况。当一个流入协议大小比较大的时候,前端进行解析时的开销也比较大。如果能把这部分负担转移到编译时去完成的话,前端依赖包大小就会减少许多。从而也提升了加载速度,降低了带宽消耗。页面越简单,这其中的 gap 就会越明显。 + +### 场景二:老项目 + 新需求,想用搭建产出 +这是一个很常见的场景,毕竟迁移或者重构都是有一个过程的,阿里的业务都是一边跑一边换发动机。在这种场景中,我们不可能要求使用运行时方案来做实现,因为运行时是一个项目级别的能力,最好是项目中统一使用他这一种方式,保证体验的一致性与连贯性。所以我们可以只在低代码平台上搭建新的业务页面,然后通过出码模块导出这些页面的源码,连同一些全局依赖模块,一起 Merge 到老项目中。完成开发体验的优化。 + +### 场景三:协议不能描述部分代码逻辑(协议功能不足或特别定制化的逻辑) +当我们发现一些逻辑诉求不能在目前协议中很好地表达的时候,这其实是项目复杂度较高的一个信号。比较好的方式就是将低代码研发和源码研发结合起来。这种模式下最大的诉求点之一就是,需要将搭建的内容输出为可读性和确定性都比较良好的代码模块。这也就是出码模块需要支持好的使用场景了。 + +## 如何使用 +### 1) 通过命令行快速体验 + +欢迎使用命令行工具快速体验:`npx @alilc/lowcode-code-generator -i example-schema.json -o generated -s icejs3` + +--其中 example-schema.json 可以从[这里下载](https://alifd.alicdn.com/npm/@alilc/lowcode-code-generator@latest/example-schema.json) + +### 2) 通过设计器插件快速体验 + +1. 安装依赖: `npm install --save @alilc/lowcode-plugin-code-generator` +2. 注册插件: + +```typescript +import { plugins } from '@alilc/lowcode-engine'; +import CodeGenPlugin from '@alilc/lowcode-plugin-code-generator'; + +// 在你的初始化函数中: +await plugins.register(CodeGenPlugin); + +// 如果您不希望自动加上出码按钮,则可以这样注册 +await plugins.register(CodeGenPlugin, { disableCodeGenActionBtn: true }); +``` + +然后运行你的低代码编辑器项目即可 -- 在设计器的右上角会出现一个“出码”按钮,点击即可在浏览器中出码并预览。 + +### 3)服务端出码接入 + +此代码生成器一开始就是为服务端出码设计的,你可以直接这样来在 node.js 环境中使用: + +1. 安装依赖: `npm install --save @alilc/lowcode-code-generator` +2. 引入代码生成器: + +```javascript +import CodeGenerator from '@alilc/lowcode-code-generator'; +``` + +3. 创建项目构建器: + +```javascript +const projectBuilder = CodeGenerator.solutions.icejs(); +``` + +4. 生成代码 + +```javascript +const project = await projectBuilder.generateProject( + schema, // 编排搭建出来的 schema +); +``` + +5. 将生成的代码写入到磁盘中 (也可以生成一个 zip 包) + +```javascript +// 写入磁盘 +await CodeGenerator.publishers.disk().publish({ + project, // 上一步生成的 project + outputPath: '/path/to/your/output/dir', // 输出目录 + projectSlug: 'your-project-slug', // 项目标识 +}); + +// 写入到 zip 包 +await CodeGenerator.publishers.zip().publish({ + project, // 上一步生成的 project + outputPath: '/path/to/your/output/dir', // 输出目录 + projectSlug: 'your-project-slug', // 项目标识 -- 对应生成 your-project-slug.zip 文件 +}); +``` + +注:一般来说在服务端出码可以跟 github/gitlab, CI 和 CD 流程等一起串起来使用,通常用于优化性能。 + +### 4)浏览器中出码接入 + +随着现在电脑性能和浏览器技术的发展,出码其实已经不必非得在服务端做了,借助于 Web Worker 特性,可以在浏览器中进行出码: + +1. 安装依赖: `npm install --save @alilc/lowcode-code-generator` +2. 引入代码生成器: + +```javascript +import * as CodeGenerator from '@alilc/lowcode-code-generator/standalone-loader'; +``` + +3. 【可选】提前初始化代码生成器: + +```javascript +// 提前初始化下,这样后面用的时候更快 (这个 init 内部会提前准备好创建 worker 的一些资源) +await CodeGenerator.init(); +``` + +4. 出码 + +```javascript +const result = await CodeGenerator.generateCode({ + solution: 'icejs', // 出码方案 (目前内置有 icejs、icejs3 和 rax ) + schema, // 编排搭建出来的 schema +}); + +console.log(result); // 出码结果 (默认是递归结构描述的,可以传 flattenResult: true 以生成扁平结构的结果) +``` + +注:一般来说在浏览器中出码适合做即时预览功能。 + +### 5)自定义出码 +前端框架灵活多变,默认内置的出码方案很难满足所有人的需求,好在此代码生成器支持非常灵活的插件机制 -- 内置功能大多都是通过插件完成的(在 `src/plugins`下),比如: +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01CEl2Hq1omnH0UCyGF_!!6000000005268-2-tps-457-376.png) + +所以您可以通过添加自己的插件或替换掉默认内置的插件来实现您的自定义功能。 +为了方便自定义出码方案,出码模块还提供自定义出码方案的脚手架功能,即执行下面脚本即可生成一个自定义出码方案: +```shell +npx @alilc/lowcode-code-generator init-solution +``` +里面内置了一个示例的插件 (在 `src/plugins/example.ts`中),您可以根据注释引导来完善相关插件,从而组合生成您的专属出码方案 (`src/index.ts`)。您所生成的出码方案可以发布成 NPM 包,从而能按上文 1~4 中的使用方案进行使用。 diff --git a/docs/docs/guide/expand/runtime/renderer.md b/docs/docs/guide/expand/runtime/renderer.md new file mode 100644 index 0000000000..4e6d914bbf --- /dev/null +++ b/docs/docs/guide/expand/runtime/renderer.md @@ -0,0 +1,348 @@ +--- +title: 使用渲染模块 +sidebar_position: 0 +--- +## 快速使用 +渲染依赖于 schema 和 components。其中 schema 和 components 需要一一对应,schema 中使用到的组件都需要在 components 中进行声明,否则无法正常渲染。 +### 简单示例 + +```jsx +import ReactRenderer from '@alilc/lowcode-react-renderer'; +import ReactDOM from 'react-dom'; +import { Button } from '@alifd/next'; + +const schema = { + componentName: 'Page', + props: {}, + children: [ + { + componentName: 'Button', + props: { + type: 'primary', + style: { + color: '#2077ff' + }, + }, + children: '确定', + }, + ], +}; + +const components = { + Button, +}; + +ReactDOM.render(( + +), document.getElementById('root')); +``` + +#### +### 项目使用示例 +> [设计器 demo](https://lowcode-engine.cn/demo/demo-general/index.html) +> 项目代码完整示例:[https://github.com/alibaba/lowcode-demo](https://github.com/alibaba/lowcode-demo) + +**step 1:在设计器中获取组件列表** +```typescript +import { material, project } from '@alilc/lowcode-engine'; +const packages = material.getAssets().packages +``` +**step 2:在设计器中获取当前配置页面的 schema** +```typescript +import { material, project } from '@alilc/lowcode-engine'; + +const schema = project.exportSchema(); +``` + + +**step 3:以某种方式存储 schema 和 packages** +这里用 localStorage 作为存储示例,真实项目中使用数据库或者其他存储方式。 +```typescript +window.localStorage.setItem( + 'projectSchema', + JSON.stringify(project.exportSchema()) +); +const packages = await filterPackages(material.getAssets().packages); +window.localStorage.setItem( + 'packages', + JSON.stringify(packages) +); +``` +**step 4:预览时,获取存储的 schema 和 packages** +```typescript +const packages = JSON.parse(window.localStorage.getItem('packages') || ''); +const projectSchema = JSON.parse(window.localStorage.getItem('projectSchema') || ''); +const { componentsMap: componentsMapArray, componentsTree } = projectSchema; +``` +**step 5:通过整合 schema 和 packages 信息,进行渲染** +```typescript +import ReactDOM from 'react-dom'; +import React, { useState } from 'react'; +import { Loading } from '@alifd/next'; +import { buildComponents, assetBundle, AssetLevel, AssetLoader } from '@alilc/lowcode-utils'; +import ReactRenderer from '@alilc/lowcode-react-renderer'; +import { injectComponents } from '@alilc/lowcode-plugin-inject'; + +const SamplePreview = () => { + const [data, setData] = useState({}); + + async function init() { + // 渲染前置处理,初始化项目 schema 和资产包为渲染模块所需的 schema prop 和 components prop + const packages = JSON.parse(window.localStorage.getItem('packages') || ''); + const projectSchema = JSON.parse(window.localStorage.getItem('projectSchema') || ''); + const { componentsMap: componentsMapArray, componentsTree } = projectSchema; + const componentsMap: any = {}; + componentsMapArray.forEach((component: any) => { + componentsMap[component.componentName] = component; + }); + const schema = componentsTree[0]; + + const libraryMap = {}; + const libraryAsset = []; + packages.forEach(({ package: _package, library, urls, renderUrls }) => { + libraryMap[_package] = library; + if (renderUrls) { + libraryAsset.push(renderUrls); + } else if (urls) { + libraryAsset.push(urls); + } + }); + + const vendors = [assetBundle(libraryAsset, AssetLevel.Library)]; + + const assetLoader = new AssetLoader(); + await assetLoader.load(libraryAsset); + const components = await injectComponents(buildComponents(libraryMap, componentsMap)); + + setData({ + schema, + components, + }); + } + + const { schema, components } = data; + + if (!schema || !components) { + init(); + return ; + } + + return ( +
+ +
+ ); +}; + +ReactDOM.render(, document.getElementById('ice-container')); + +``` +### 国际化示例 +```typescript +class Demo extends PureComponent { + static displayName = 'renderer-demo'; + render() { + return ( +
+ +
+ ); + } +} +``` + +## API + +| 参数 | 说明 | 类型 | 必选 | +| --- | --- | --- | --- | +| schema | 符合[搭建协议](https://lowcode-engine.cn/lowcode)的数据 | Object | 是 | +| components | 组件依赖的实例 | Object | 是 | +| componentsMap | 组件的配置信息 | Object | 否 | +| appHelper | 渲染模块全局上下文 | Object | 否 | +| designMode | 设计模式,可选值:extend、border、preview | String | 否 | +| suspended | 是否挂起 | Boolean | 否 | +| onCompGetRef | 组件 ref 回调(schema, ref)=> {} | Function | 否 | +| onCompGetCtx | 组件 ctx 更新回调 (schema, ctx) => {} | Function | 否 | +| rendererName | 渲染类型,标识当前模块是以什么类型进行渲染的 | string | 否 | +| customCreateElement | 自定义创建 element 的钩子 +(Component, props, children) => {} | Function | 否 | +| notFoundComponent | 当组件找不到时,可以通过这个参数自定义展示文案。 | Component | 否 | +| thisRequiredInJSE | 为 true 的情况下 JSExpression 仅支持通过 this 来访问。假如需要兼容原来的 'state.xxx',则设置为 false,推荐使用 true。 | Boolean | 否 | +| locale | 国际化语言类型 | string | 否 | +| messages | 国际化语言对象 | Object | 否 | + + +### schema + +搭建基础协议数据,渲染模块将基于 schema 中的内容进行实时渲染。 + +### messages +国际化内容,需要配合 locale 使用 +messages 格式示例: +```typescript +{ + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +} +``` + +### locale +当前语言类型 +示例:'zh-CN' | 'en-US' + +### components + +渲染模块渲染页面需要用到的组件依赖的实例,`components` 对象中的 Key 需要和搭建 schema 中的`componentName` 字段对应。 + +### componentsMap + +> 在生产环境下不需要设置。 + + +配置规范参见[《低代码引擎搭建协议规范》](https://lowcode-engine.cn/lowcode),主要在搭建场景中使用,用于提升用户搭建体验。 + +- 属性配置校验:用户可以配置组件特定属性的 `propTypes`,在搭建场景中用户输入的属性值不满足 `propType` 配置时,渲染模块会将当前属性设置为 `undefined`,避免组件抛错导致编辑器崩溃; +- `isContainer` 标记:当组件被设置为容器组件且当前容器组件内没有其他组件时,用户可以通过拖拽方式将组件直接添加到容器组件内部; +- `parentRule` 校验:当用户使用的组件未出现在组件配置的 `parentRule` 组件内部时,渲染模块会使用 `visualDom` 组件进行占位,避免组件抛错的同时在下钻编辑场景也能够不阻塞用户配置,典型的场景如`Step.Item`、`Table.Column`、`Tab.Item` 等等。 + +### appHelper + +appHelper 主要用于设置渲染模块的全局上下文,目前 appHelper 支持设置以下上下文: + +- `utils`:全局公共函数 +- `constants`:全局常量 +- `location`:react-router 的 `location` 实例 +- `history`:react-router 的 `history` 实例 + +设置了 appHelper 以后,上下文会直接挂载到容器组件的 this 上,用户可以在搭建协议中的 function 及变量表达式场景使用上下文,具体使用方式如下所示: +**schema:** + +```javascript +export default { + "componentName": "Page", + "fileName": "test", + "props": {}, + "children": [{ + "componentName": "Div", + "props": {}, + "children": [{ + "componentName": "Text", + "props": { + "text": { + "type": "JSExpression", + "value": "this.location.pathname" + } + } + }, { + "componentName": "Button", + "props": { + "type": "primary", + "style": { + "marginLeft": 10 + }, + "onClick": { + "type": "JSExpression", + "value": "function onClick(e) { this.utils.xxx(this.constants.yyy);}" + } + }, + "children": "click me" + }] + }] +} +``` + +```typescript +import ReactRenderer from '@alilc/lowcode-react-renderer'; +import ReactDOM from 'react-dom'; +import { Button } from '@alifd/next'; +import schema from './schema' + +const components = { + Button, +}; + +ReactDOM.render(( + {} + } + }} + /> +), document.getElementById('root')); +``` +### designMode + +> 在生产环境下不需要设置。 + + +designMode 属性主要在搭建场景中使用,主要有以下作用: + +- 当 `designMode` 改变时,触发当前 schema 中所有组件重新渲染 +- 当 `designMode` 设置为 `design` 时,渲染模块会为 `Dialog`、`Overlay` 等初始状态无 dom 渲染的组件外层包裹一层 div,使其在画布中能够展示边框供用户选中 + +### suspended + +渲染模块是否挂起,当设置为 `true` 时,渲染模块最外层容器的 `shouldComponentUpdate`将始终返回 false,在下钻编辑或者多引擎渲染的场景会用到该参数。 + +### onCompGetRef + +组件 ref 的回调,在搭建场景下编排模块可以根据该回调获取组件实例并实现生命周期注入或者组件 DOM 操作等功能,回调函数主要包含两个参数: + +- `schema`:当前组件的 schema 模型结构 +- `ref`:当前组件的 ref 实例 + +### onCompGetCtx +组件 ctx 更新的回调,在组件每次 render 渲染周期我们都会为组件构造新的上下文环境,因此该回调函数会在组件每次 render 过程中触发,主要包含两个参数: + +- `schema`:当前组件的 schema 模型结构 +- `ctx`:当前组件的上下文信息,主要包含以下内容: + - `page`:当前页面容器实例 + - `this`: 当前组件所属的容器组件实例 + - `item`/`index`: 循环上下文(属性 key 可以根据 loopArgs 进行定制) + - `form`: 表单上下文 + +### rendererName +渲染类型,标识当前模块是以什么类型进行渲染的 + +- `LowCodeRenderer`: 低代码组件 +- `PageRenderer`: 页面 + +### customCreateElement +自定义创建 element 的钩子,用于在渲染前后对组件进行一些处理,包括但不限于增加 props、删除部分 props。主要包含三个参数: + +- `Component`:要渲染的组件 +- `props`:要渲染的组件的 props +- `children`:要渲染的组件的子元素 + +### thisRequiredInJSE +> 版本 >= 1.0.11 + +默认值:true +为 true 的情况下 JSExpression 仅支持通过 this 来访问。假如需要兼容原来的 'state.xxx',则设置为 false,推荐使用 true。 diff --git a/docs/docs/guide/quickStart/_category_.json b/docs/docs/guide/quickStart/_category_.json new file mode 100644 index 0000000000..0a47c9da50 --- /dev/null +++ b/docs/docs/guide/quickStart/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "入门", + "position": 0, + "collapsed": false, + "collapsible": true +} diff --git a/docs/docs/guide/quickStart/demo.md b/docs/docs/guide/quickStart/demo.md new file mode 100644 index 0000000000..ee76d5aa1b --- /dev/null +++ b/docs/docs/guide/quickStart/demo.md @@ -0,0 +1,56 @@ +--- +title: 试用低代码引擎 Demo +sidebar_position: 2 +--- +## 访问地址 + +低代码引擎的 Demo 可以通过如下永久链接访问到: + +[设计器 demo](https://lowcode-engine.cn/demo/demo-general/index.html) + +> 注意我们会经常更新 demo,所以您可以通过上述链接得到最新版地址。 + + +## 低代码引擎 Demo 功能概览 + +我们可以从 Demo 的项目中看到页面中有很多的区块: + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01vlxdTD28c4JZcebbf_!!6000000007952-2-tps-3840-2160.png) + +它主要包含这些功能点: + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01QITHRY1sQaWzlvJv9_!!6000000005761-2-tps-3840-2160.png) + +### 顶部:操作区 + +- 右侧:撤回和重做、保存到本地、重置页面、预览、异步加载资源 +### 左侧:面板与操作区 +- 大纲面板:可以调整页面内的组件树结构 +- 物料面板:可以查找组件,并在此拖动组件到编辑器画布中 +- 源码面板:可以编辑页面级别的 JavaScript 代码和 CSS 配置 +- 提交 Issue:可以给引擎开发提 bug +- Schema 编辑:可以编辑页面的底层数据 +- 中英文切换:可以切换编辑器的语言 + +### 中部:可视化页面编辑画布区域 +- 点击组件在右侧面板中能够显示出对应组件的属性配置选项 +- 拖拽修改组件的排列顺序 +- 将组件拖拽到容器类型的组件中 +- 复制组件:点击组件右上角的复制按钮 +- 删除组件:点击组件右上角的 X 或者直接使用 `Delete` 键 + +### 右侧:组件级别配置 +- 选中的组件:从页面开始一直到当前选中的组件位置,点击对应的名称可以切换到对应的组件上 +- 选中组件的配置:当前组件的大类目选项,根据组件类型不同,包含如下子类目: + - 属性:组件的基础属性值设置 + - 样式:组件的样式配置 + - 事件:绑定组件对外暴露的事件 + - 高级:循环、条件渲染与 key 设置 + +## 深入使用低代码引擎 Demo + +我们在低代码引擎 Demo 中直接内置了产品使用文档,对常见场景中的使用进行了向导,它的入口如下: + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01YU2LYS29YEbuLTtLL_!!6000000008079-2-tps-3070-1650.png) + +如果暂时没有看到对应的产品使用文档,可以通过此永久链接直接访问:[https://lowcode-engine.cn/site/docs/demoUsage/intro](https://lowcode-engine.cn/site/docs/demoUsage/intro) diff --git a/docs/docs/guide/quickStart/intro.md b/docs/docs/guide/quickStart/intro.md new file mode 100644 index 0000000000..b65baac269 --- /dev/null +++ b/docs/docs/guide/quickStart/intro.md @@ -0,0 +1,63 @@ +--- +title: 简介 +sidebar_position: 1 +--- + +# 阿里低代码引擎简介 + +## 低代码介绍 + +零代码、低代码的概念在整个全球行业内已经流行了很长一段时间。通常意义上的低代码定义会有三个关键点: + +1. 一个用于生产软件的可视化编辑器 +2. 中间包含了一些用于组装的物料,可以通过编排、组合和配置它们以生成丰富的功能或表现 +3. 最后的实施结果是成本降低 + +通常情况下低代码平台会具备以下的几个能力: + +- **可视化页面搭建**,通过简单的拖拽完成应用页面开发,对前端技能没有要求或不需要特别专业的了解; +- **可视化模型设计**,与业务相关的数据存储变得更容易理解,甚至大多数简单场景可以做到表单即模型,模型字段的类型更加业务化; +- **可视化流程设计**,不管是业务流程还是审批流程,都可以通过简单的点线连接来进行配置; +- **可视化报表及数据分析**,BI 数据分析能力成为标配,随时随地通过拖拽选择来定义自定义分析报表; +- **可视化服务与数据开放、集成**,具备与其他系统互联互通的配置; +- **权限、角色设置标准化和业务化**,通过策略规则配置来将数据、操作的权限进行精细化管理; +- **无需关心服务器、数据库等底层运维、计算设施设备、网络等等复杂技术概念**,具备安全、性能的统一解决方案,开发者只需要专注于业务本身; + +有了上面这些,你会发现即使是个技术小白,只要你了解业务,就能不受束缚的完成大多数业务应用的搭建。但低代码本身也不仅仅是为技术小白准备的。在实践中,低代码因为通过组件化、模块化的思路让业务的抽象更加容易,而且在扩展及配置化上带来了更加新鲜的模式探索,技术人员的架构设计成本和实施成本也就降了很多。 + +市面上常见的低代码产品[可以看 Golden 的梳理](https://golden.com/wiki/No-code_%2F_low-code_development-NMGMEA6)。 + +## 低代码引擎介绍 + +**低代码引擎是一款为低代码平台开发者提供的,具备强大定制扩展能力的低代码设计器研发框架。** + +下面简单描述定义中的子部分: + +**低代码设计器** +现如今低代码平台越来越多,而每一个低代码平台中都会有的一个能力就是搭建和配置页面、模块的页面,这个页面我们称为设计器。例如,下图是中后台低代码平台的设计器。 +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01sXuwkK1j8sg4S53Dx_!!6000000004504-2-tps-1682-969.png) +设计器承载着低代码平台的核心功能,包括入料、编排、组件配置、画布渲染等等。由于其功能多,打磨精细难,也是低代码平台建设最耗时的地方。 + +**定制扩展能力** + +什么是扩展能力呢,一方面我们可以快速拥有一份标准的低代码设计器,另外一方面如果有业务独特的功能需要,我们可以不用看它的源码、不用关心其实现,可以使用 API、插件等方式快速完成能力的开发。 +而低代码引擎对于设计器的扩展能力支持基本上覆盖了低代码设计器的所有功能点。下图是针对标准的设计器提供了扩展功能的区域。 +![](https://img.alicdn.com/imgextra/i1/O1CN01ZVgAE31wltQ4BVnCe_!!6000000006349-2-tps-3838-1914.png) +**低代码设计器研发框架** + +低代码引擎的核心是设计器,通过扩展、周边生态等可以产出各式各样的设计器。它不是一套可以适合所有人的低代码平台,而是帮助低代码平台的开发者,快速生产低代码平台的工具。 + +## 寻找适合您的低代码解决方案 + +帮助用户根据个人或企业需求选择合适的低代码产品。 + +| 特性/产品 | 低代码引擎 | Lab平台 | UIPaaS | +|-----------------|-----------------------------------------|-----------------------------------------|--------------------------------------------| +| **适用用户** | 前端开发者 | 需要快速搭建应用/页面的用户 | 企业用户,需要大规模部署低代码解决方案的组织 | +| **产品特点** | 设计器研发框架,适合定制开发 | 低代码平台, 可视化操作界面,易于上手 | 低代码平台孵化器,企业级功能 | +| **使用场景** | 定制和开发低代码平台的设计器部分 | 通过可视化, 快速开发应用或页面 | 帮助具有一定规模软件研发团队的的企业低成本定制低代码平台 | +| **产品关系** | 开源产品 | 基于UIPaaS技术实现, 展示了UIPaaS的部分能力 | 提供完整的低代码平台解决方案,商业产品 | +| **收费情况** | 免费 | 可免费使用(有额度限制),不提供私有化部署售卖 | 仅提供私有化部署售卖 | +| **官方网站** | [低代码引擎官网](https://lowcode-engine.cn/) | [Lab平台官网](https://lab.lowcode-engine.cn/) | [UIPaaS官网](https://uipaas.net/) | + +*注:请根据您的具体需求和条件选择合适的产品。如需更详细的信息,请访问各产品的官方网站。* diff --git a/docs/docs/guide/quickStart/start.md b/docs/docs/guide/quickStart/start.md new file mode 100644 index 0000000000..356f501769 --- /dev/null +++ b/docs/docs/guide/quickStart/start.md @@ -0,0 +1,411 @@ +--- +sidebar_position: 3 +title: 快速开始 +--- + +## 前置知识 + +我们假定你已经对 HTML 和 JavaScript 都比较熟悉了。即便你之前使用其他编程语言,你也可以跟上这篇教程的。除此之外,我们假定你也已经熟悉了一些编程的概念,例如,函数、对象、数组,以及 class 的一些内容。 + +如果你想回顾一下 JavaScript,你可以阅读[这篇教程](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/A_re-introduction_to_JavaScript)。注意,我们也用到了一些 ES6(较新的 JavaScript 版本)的特性。在这篇教程里,我们主要使用了[箭头函数(arrow functions)](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions)、[class](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes)、[let](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let) 语句和 [const](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/const) 语句。你可以使用 [Babel REPL](https://babeljs.io/repl/#?presets=react&code_lz=MYewdgzgLgBApgGzgWzmWBeGAeAFgRgD4AJRBEAGhgHcQAnBAEwEJsB6AwgbgChRJY_KAEMAlmDh0YWRiGABXVOgB0AczhQAokiVQAQgE8AkowAUAcjogQUcwEpeAJTjDgUACIB5ALLK6aRklTRBQ0KCohMQk6Bx4gA) 在线预览 ES6 的编译结果。 + +## 环境准备 + +### WSL(Windows 电脑) + +Window 环境需要使用 WSL 在 windows 下进行低代码引擎相关的开发。安装教程 ➡️ [WSL 安装教程](https://docs.microsoft.com/zh-cn/windows/wsl/install)。
**对于 Window 环境来说,之后所有需要执行命令的操作都是在 WSL 终端执行的。** + +### Node + +node 版本推荐 16.18.0。 + +#### 查看 Node 版本 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01oCZKNz290LIu8YUTk_!!6000000008005-2-tps-238-70.png) + +#### 通过 n 来管理 node 版本 + +可以安装 [n](https://www.npmjs.com/package/n) 来管理和变更 node 版本。 + +##### 安装 n + +```bash +npm install -g n +``` + +##### 变更 node 版本 + +```bash +n 14.17.0 +``` + +### React + +低代码引擎的扩展能力都是基于 React 来研发的,在继续阅读之前最好有一定的 React 基础,React 学习教程 ➡️ [React 快速开始教程](https://zh-hans.reactjs.org/docs/getting-started.html)。 + +### 下载 Demo + +可以前往 github()将 DEMO 下载到本地。 + +#### git clone + +##### HTTPS + +需要使用到 git 工具 + +```bash +git clone https://github.com/alibaba/lowcode-demo.git +``` + +##### SSH + +需要配置 SSH key,如果没有配置可以 + +```bash +git clone git@github.com:alibaba/lowcode-demo.git +``` + +#### 下载 Zip 包 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01iYC7E11phaNwLFUrN_!!6000000005392-2-tps-3584-1794.png) + +### 选择一个 demo 项目 + +在 以 `demo-general` 为例: + +```bash +cd demo-general +``` + +### 安装依赖 + +在 `lowcode-demo/demo-general` 目录下执行: + +```bash +npm install +``` + +### 启动 demo + +在 `lowcode-demo/demo-general` 目录下执行: + +```bash +npm run start +``` + +之后就可以通过 [http://localhost:5556/](http://localhost:5556/) 来访问我们的 DEMO 了。 + +## 认识 Demo + +我们的 Demo 是一个**低代码平台的设计器**。它是一个低代码平台中最重要的一环,用户可以在这里通过拖拽、配置、写代码等等来完成一个页面的开发。由于用户的人群不同、场景不同、诉求不同等等,这个页面的功能就会有所差异。 + +这里记住**设计器**这个词,它描述的就是下面的这个页面,后面我们会经常看到它。 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN014nYXgF20pKrQIG2zV_!!6000000006898-2-tps-3584-1808.png) + +### 场景介绍 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01nnP60l1dqUhUiNSx6_!!6000000003787-2-tps-2852-1156.png) + +Demo 根据**不同的设计器所需要的物料不同**,分为了下面的 8 个场景: + +- 综合场景 +- 基础 fusion 组件 +- 基础 fusion 组件 + 单自定义组件 +- 基础 antd 组件 +- 自定义初始化引擎 +- 扩展节点操作项 +- 基于 next 实现的高级表单低代码物料 +- antd 高级组件 + formily 表单组件 + +可以点开不同的场景,看看他们使用的物料。 +![](https://img.alicdn.com/imgextra/i1/O1CN01EU2jRN1wUwlal17WK_!!6000000006312-2-tps-3110-1974.png) + +### 目录介绍 + +仓库下每个 demo-xxx-xxx 目录都是一个可独立运行的 demo 工程,分别对应到刚刚介绍的场景。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01ztxv5Y1mJozBsLdni_!!6000000004934-2-tps-696-958.png) + +不同场景的目录结构实际上都是类似的,这里我们主要介绍一下综合场景的目录结构即可。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01A50oW522S5zg2eDUH_!!6000000007118-2-tps-732-1384.png) + +介绍下其中主要的内容 + +- 设计器入口文件 `src/index.ts` 这个文件做了下述几个事情: + - 通过 plugins.register 注册各种插件,包括官方插件 (已发布 npm 包形式的插件) 和 `plugins` 目录下内置的示例插件 + - 通过 init 初始化低代码设计器 +- plugins 目录,存放的都是示例插件,方便用户从中看到一个插件是如何实现的 +- services 目录,模拟数据请求、提供默认 schema、默认资产包等,此目录下内容在真实项目中应替换成真实的与服务端交互的服务。 +- 预览页面入口文件 `preview.tsx` + +剩下的各位看官可以通过源码来进一步了解。 + +做了这些事情之后,我们的低代码设计器就已经有了基本的能力了。也就是最开始我们看到的这样。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01YJVcOd1PiL1am6bz2_!!6000000001874-2-tps-3248-1970.png) + +接下来我们就根据我们自己的诉求通过对设计器进行扩展,改动成我们需要的设计器功能。 + +## 开发一个插件 + +### 方式 1:在 DEMO 中直接新增插件 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01pXpSRs1QvRyut2EE3_!!6000000002038-2-tps-718-1144.png) + +可以在 demo/sample-plugins 直接新增插件,这里我新增的插件目录是 plugin-demo。并且新增了 index.tsx 文件,将下面的代码粘贴到 index.tsx 中。 + +```javascript +import * as React from 'react'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +const LowcodePluginPluginDemo = (ctx: IPublicModelPluginContext) => { + return { + // 插件对外暴露的数据和方法 + exports() { + return { + data: '你可以把插件的数据这样对外暴露', + func: () => { + console.log('方法也是一样'); + }, + }; + }, + // 插件的初始化函数,在引擎初始化之后会立刻调用 + init() { + // 你可以拿到其他插件暴露的方法和属性 + // const { data, func } = ctx.plugins.pluginA; + // func(); + + // console.log(options.name); + + // 往引擎增加面板 + ctx.skeleton.add({ + area: 'leftArea', + name: 'LowcodePluginPluginDemoPane', + type: 'PanelDock', + props: { + description: 'Demo', + }, + content:
这是一个 Demo 面板
, + }); + + ctx.logger.log('打个日志'); + }, + }; +}; + +// 插件名,注册环境下唯一 +LowcodePluginPluginDemo.pluginName = 'LowcodePluginPluginDemo'; +LowcodePluginPluginDemo.meta = { + // 依赖的插件(插件名数组) + dependencies: [], + engines: { + lowcodeEngine: '^1.0.0', // 插件需要配合 ^1.0.0 的引擎才可运行 + }, +}; + +export default LowcodePluginPluginDemo; +``` + +在 src/index.ts 中新增下面代码 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01pNTr4N1kldoYZRzgI_!!6000000004724-2-tps-1976-1250.png) + +这样在我们的设计器中就新增了一个 Demo 面板。 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01wtPIOV1TQiFLz5Vkf_!!6000000002377-2-tps-3584-1806.png) + +### 方式 2:在新的仓库下开发插件 + +初始化 + +```bash +npm init @alilc/element your-plugin-name +``` + +选择设计器插件(plugin) + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01sA6sYW1tijqVeQCuq_!!6000000005936-2-tps-730-214.png) + +根据操作完善信息 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01BzM1Jb1RcxbiJ0tJi_!!6000000002133-2-tps-866-218.png) + +插件项目就初始化完成了 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01iVIAXD1XVWsOdKttI_!!6000000002929-2-tps-3584-2020.png) + +在插件项目下安装依赖 + +```bash +npm install +``` + +启动项目 + +```bash +npm run start +``` + +调试项目 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01A4vPqC1xbeAqNxBRM_!!6000000006462-2-tps-3584-1936.png) + +在 Demo 中调试项目 + +在 build.json 下面新增 "inject": true,就可以在 [https://lowcode-engine.cn/demo/demo-general/index.html?debug](https://lowcode-engine.cn/demo/demo-general/index.html?debug) 页面下进行调试了。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01uqSmrX1oqupxeGH1m_!!6000000005277-2-tps-3584-2020.png) + +## 开发一个自定义物料 + +### 初始化物料 + +```bash +npm init @alilc/element your-material-demo +``` + +选择组件/物料栏 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN01qVJQvG1Yhj2PJhhvk_!!6000000003091-2-tps-824-208.png) + +配置其他信息 + +![image.png](https://img.alicdn.com/imgextra/i3/O1CN017fFT8O1IVmrLYg87F_!!6000000000899-2-tps-800-248.png) + +这样我们就初始化好了一个 React 物料。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN01SU2xn91TZPlzcARVI_!!6000000002396-2-tps-3584-2020.png) + +### 启动并调试物料 + +#### 安装依赖 + +```bash +npm i +``` + +#### 启动 + +```bash +npm run lowcode:dev +``` + +我们就可以通过 [http://localhost:3333/](http://localhost:3333/) 看到我们的研发的物料了。 + +![image.png](https://img.alicdn.com/imgextra/i4/O1CN01JqoHqc1z7zlSWFYJD_!!6000000006668-2-tps-3584-1790.png) + +#### 在 Demo 中调试 + +```bash +npm i @alilc/build-plugin-alt +``` + +修改 build.lowcode.js + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01K7u7ci1KCfYlBj2yf_!!6000000001128-2-tps-1388-1046.png) + +如图,新增如下代码 + +```javascript +[ + '@alilc/build-plugin-alt', + { + type: 'component', + inject: true, + library, + // 配置要打开的页面,在注入调试模式下,不配置此项的话不会打开浏览器 + // 支持直接使用官方 demo 项目:https://lowcode-engine.cn/demo/index.html + openUrl: 'https://lowcode-engine.cn/demo/index.html?debug', + }, +], +``` + +我们重新启动项目,就可以在 Demo 中找到我们的自定义组件。 + +![image.png](https://img.alicdn.com/imgextra/i1/O1CN0166WywE26Lv7NuJMus_!!6000000007646-2-tps-3584-1812.png) + +### 发布 + +首先进行构建 + +```bash +npm run lowcode:build +``` + +发布组件 + +```bash +npm publish +``` + +这里我发布的组件是 [my-material-demo](https://www.npmjs.com/package/my-material-demo)。在发布之后我们就会有两个重要的文件: + +- 低代码描述:[https://unpkg.com/my-material-demo@0.1.0/build/lowcode/meta.js](https://unpkg.com/my-material-demo@0.1.0/build/lowcode/meta.js) +- 组件代码:[https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.js](https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.js) + +我们也可以从 [https://unpkg.com/my-material-demo@0.1.0/build/lowcode/assets-prod.json](https://unpkg.com/my-material-demo@0.1.0/build/lowcode/assets-prod.json) 找到我们的资产包描述。 + +```bash +{ + "packages": [ + { + "package": "my-material-demo", + "version": "0.1.0", + "library": "BizComp", + "urls": [ + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.js", + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.css" + ], + "editUrls": [ + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/view.js", + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/view.css" + ], + "advancedUrls": { + "default": [ + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.js", + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/render/default/view.css" + ] + }, + "advancedEditUrls": {} + } + ], + "components": [ + { + "exportName": "MyMaterialDemoMeta", + "npm": { + "package": "my-material-demo", + "version": "0.1.0" + }, + "url": "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/meta.js", + "urls": { + "default": "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/meta.js" + }, + "advancedUrls": { + "default": [ + "https://unpkg.com/my-material-demo@0.1.0/build/lowcode/meta.js" + ] + } + } + ], +} +``` + +### 使用 + +我们将刚刚发布的组件的 assets-prod.json 的内容放到 demo 的 src/universal/assets.json 中。 + +> 最好放到最后,防止因为资源加载顺序问题导致出现报错。 + +如图,新增 packages 配置 +![image.png](https://img.alicdn.com/imgextra/i1/O1CN018dnIB91XHmzeTrq3n_!!6000000002899-2-tps-3584-2020.png) + +如图,新增 components 配置 + +![image.png](https://img.alicdn.com/imgextra/i2/O1CN01UNp89s1vQXKyfsFaL_!!6000000006167-2-tps-3584-2020.png) + +这时候再启动 DEMO 项目,就会有新的低代码物料了。接下来就按照你们的需求,继续扩展物料吧。 + +## 总结 + +这里只是简单的介绍了一些低代码引擎的基础能力,带大家简单的对低代码 DEMO 进行扩展,定制一些新的功能。低代码引擎的能力还有很多很多,可以继续去探索更多的功能。 diff --git a/docs/code-specification.md b/docs/docs/participate/code-specification.md similarity index 90% rename from docs/code-specification.md rename to docs/docs/participate/code-specification.md index 0a7c9f5556..d6b387e305 100644 --- a/docs/code-specification.md +++ b/docs/docs/participate/code-specification.md @@ -1,3 +1,8 @@ +--- +title: 编码规约 +sidebar_position: 5 +--- + 编码规约 --- @@ -22,7 +27,7 @@ - 不要在全局命名空间内定义类型/值 - 共享的类型应该在 `types.ts` 里定义 - 在一个文件里,类型定义应该出现在顶部 - - interface 和 type 很类似,原则上能用 interface 实现,就用 interface , 如果不能才用 type + - interface 和 type 很类似,原则上能用 interface 实现,就用 interface , 如果不能才用 type ### 注释 diff --git a/docs/docs/participate/flow.md b/docs/docs/participate/flow.md new file mode 100644 index 0000000000..b8b804e123 --- /dev/null +++ b/docs/docs/participate/flow.md @@ -0,0 +1,187 @@ +--- +title: 研发协作流程 +sidebar_position: 2 +--- +## 代码风格 +引擎项目配置了 eslint 和 stylelint,在每次 git commit 前都会检查代码风格,假如有报错,请修改后再提交。(**严禁 -n 提交,-n 也逃脱不了 github workflow 的 lint 检查,放弃吧,骚年~**) + +## 测试机制 +每次提交代码前,务必本地跑一次单元测试,通过后再提交 MR。 + +假如涉及新的功能,需要**补充相应的单元测试**,目前引擎核心模块的单测覆盖率都在 80%+,假如降低了覆盖率,将会不予以通过。 + +跑单测流程: + +1. 项目根目录下执行 npm run build +2. 只改了一个包,比如 designer,则在 designer 目录下,执行 npm test +3. (or)改了多个包,则在根目录下执行 npm test +## commit 风格 +几点要求: + +1. commit message 格式遵循 [ConvensionalCommits](https://www.conventionalcommits.org/en/v1.0.0/#summary) + + +2. 请按照一个 bugfix / feature 对应一个 commit,假如不是,请 rebase 后再提交 MR,不要一堆无用的、试验性的 commit。 + +好处:从引擎的整体 commit 历史来看,会很清晰,**每个 commit 完成一件确定的事,changelog 也能自动生成**。另外,假如因为某个 commit 导致了 bug,也很容易通过 rebase drop 等方式快速修复。 + +## 分支用途 + +- main 分支,最稳定的分支,跟 npm latest 包的内容保持一致 +- develop 分支,开发分支,拥有最新的、已经验证过的 feature / bugfix,Pull Request 的**目标合入分支** +- release 分支 + - 正式发布分支,命名规则为 release/x.y.z,一般从 develop 拉出来进行发布,x.y.z 为待发布的版本号 + - beta 发布分支,命名规则为 release/x.y.z-beta(\.\d+)?,可以快速验证修改,发布 npm beta 版本。 + +验证通过后,因为 beta 发布分支上会存在无用的 commit(比如 lerna 修改 package.json 这种),所以不直接 PR 到 develop,而是从 develop 拉分支,从 beta 发布分支 cherry pick 有用的 commit 到新分支,然后 PR 到 develop。 + +## 引擎发布机制 + +日常迭代先从 develop 拉分支,然后自测、单测通过后,提交 PR 到 develop 分支,由发布负责人基于 develop 拉 release/1.0.z 分支~ + +### 版本规划 + +> 此处是理想节奏,实际情况可能会有调整 + +- 日常迭代 2 周,一般月中或月底,发版日两天前发最后一个 beta 版本,原则上不接受新 pr,灰度 2 天后,发正式版。 +- 特殊情况紧急迭代随时发 +- 大 Feature 迭代,每年 2 - 4 次 + + +### 发布步骤 +> **发布需要权限,如果提 PR 之后着急发布可以**[**加入贡献者交流群**](../participate/#核心贡献者交流)**。** + +#### 发正式版 +步骤如下(以发布 1.0.0 版本为例): + +1. git checkout develop + ```bash + git checkout develop + ``` +2. 创建 release 分支 + ```bash + git checkout -b release/1.0.0 + ``` +3. build + ```bash + npm run build + ``` +4. 发布到 npm + ```bash + npm run pub + ``` +5. 同步到 tnpm 源 & alifd CDN & uipaas CDN(此步骤将发布在 npm 源的包同步到阿里内网源,因为 alifd cdn 将依赖内网 npm 源) + ```bash + tnpm run sync + tnpm run syncOss + ``` +6. 更新[发布日志](https://github.com/alibaba/lowcode-engine/releases) +7. 合并 release/x.x.x 到 main 分支 +8. 合并 main 分支到 develop 分支 + +如果是发布 beta 版本,步骤如下(以发布 1.0.1 版本为例): + +#### 发某 y 位版本首个 beta,如 1.1.0-beta.0 +1. 拉 develop 分支 + ```bash + git checkout develop + ``` + 更新到最新(如需) + ```bash + git pull + ``` +2. 拉 release 分支,此处以 1.1.0 版本做示例 + ```bash + git checkout -b release/1.1.0-beta + git push --set-upstream origin release/1.1.0-beta + ``` +3. build + ```bash + npm run build + ``` +4. 发布,此处需有 @alilc scope 发包权限 + ```bash + npm run pub:preminor + ``` +5. 同步到 tnpm 源 & alifd CDN & uipaas CDN + ```bash + tnpm run sync + tnpm run syncOss + ``` + +#### 发某 z 位版本首个 beta,如 1.0.1-beta.0 +1. 拉 develop 分支 + ```bash + git checkout develop + ``` + 更新到最新(如需) + ```bash + git pull + ``` +2. 拉 release 分支,此处以 1.0.1 版本做示例 + ```bash + git checkout -b release/1.0.1-beta + git push --set-upstream origin release/1.0.1-beta + ``` +3. build + ```bash + npm run build + ``` +4. 发布,此处需有 @alilc scope 发包权限 + ```bash + npm run pub:prepatch + ``` +5. 同步到 tnpm 源 & alifd CDN & uipaas CDN + ```bash + tnpm run sync + tnpm run syncOss + ``` + +#### 发某版本非首个 beta,如 1.0.1-beta.0 -> 1.0.1-beta.1 +1. 切换到 release 分支 + ```bash + git checkout release/1.0.1-beta + ``` +2. 更新到 develop 分支最新代码 + ```bash + git rebase origin/develop + ``` +3. build + ```bash + npm run build + ``` +4. 发布,此处需有 @alilc scope 发包权限 ***此处命令与发首个 beta 时有变化*** + ```bash + npm run pub:prerelease + ``` +5. 同步到 tnpm 源 & alifd CDN & uipaas CDN + ```bash + tnpm run sync + tnpm run syncOss + ``` + + + +## DEMO 发布机制 +1. **修改版本号** + 手动修改 package.json 的版本号 +2. **build** + ```bash + npm run build + ``` +3. publish(此步骤需要 npm 发包权限) + ```bash + npm run pub + ``` + 如发 beta 版 + ```bash + npm publish --tag beta + ``` +4. 同步到 tnpm 源 & alifd CDN & uipaas CDN + ```bash + tnpm run sync + tnpm run syncOss + ``` + +**官网生效** +需要在通过阿里内部系统更新 demo 版本 diff --git a/docs/docs/participate/index.md b/docs/docs/participate/index.md new file mode 100644 index 0000000000..e09f2ddad2 --- /dev/null +++ b/docs/docs/participate/index.md @@ -0,0 +1,118 @@ +--- +title: 参与贡献 +sidebar_position: 0 +--- + +### 环境准备 + +开发 LowcodeEngine 需要 Node.js 16+。 + +推荐使用 nvm 管理 Node.js,避免权限问题的同时,还能够随时切换当前使用的 Node.js 的版本。 + +### 贡献低代码引擎 + +#### clone 项目 + +``` +git clone git@github.com:alibaba/lowcode-engine.git +cd lowcode-engine +``` + +#### 安装依赖并构建 + +``` +npm install && npm run setup +``` + +#### 调试环境配置 + +本质上是将 demo 页面引入的几个 js/css 代理到 engine 项目,可以使用趁手的代理工具,这里推荐 [XSwitch](https://chrome.google.com/webstore/detail/xswitch/idkjhjggpffolpidfkikidcokdkdaogg?hl=en-US)。 + +本地开发代理规则如下: +```json +{ + "proxy": [ + [ + "https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/js/engine-core.js", + "http://localhost:5555/js/AliLowCodeEngine.js" + ], + [ + "https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/css/engine-core.css", + "http://localhost:5555/css/AliLowCodeEngine.css" + ], + [ + "https?://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/js/react-simulator-renderer.js", + "http://localhost:5555/js/ReactSimulatorRenderer.js" + ], + [ + "https?://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/(.*)/dist/css/react-simulator-renderer.css", + "http://localhost:5555/css/ReactSimulatorRenderer.css" + ] + ] +} +``` + +#### 开发 + +``` +npm start +``` + +选择一个环境进行调试,例如[低代码引擎在线 DEMO](https://lowcode-engine.cn/demo/demo-general/index.html) + +开启代理之后,就可以进行开发调试了。 + + +### 贡献低代码引擎文档 + +#### 开发文档 + +在 lowcode-engine 目录下执行下面命令 +``` +cd docs + +npm start +``` + +#### 维护方式 +- 官方文档通过 github 管理文档源,官网文档与[主仓库 develop 分支](https://github.com/alibaba/lowcode-engine/tree/develop/docs)保持同步。 +- 点击每篇文档下发的 `编辑此页` 可直接定位到 github 中位置。 +- 欢迎 PR,文档 PR 也会作为贡献者贡献,会用于贡献度统计。 +- **文档同步到官方网站由官方人员进行操作**,如有需要可以通过 issue 或 贡献者群与相关人员沟通。 +- 为了提供更好的阅读和使用体验,文档中的图片文件会定期转换成可信的 CDN 地址。 + +#### 文档格式 + +本项目文档参考[文档编写指南](https://github.com/sparanoid/chinese-copywriting-guidelines)。 + +使用 vscode 进行编辑的朋友可以安装 vscode 插件 [huacnlee.autocorrect](https://github.com/huacnlee/autocorrect) 辅助文档 lint。 + + +### 贡献低代码引擎生态 + +相关源码详见[NPM 包对应源码位置汇总](/site/docs/guide/appendix/npms) + +开发调试方式详见[低代码生态脚手架 & 调试机制](/site/docs/guide/expand/editor/cli) + +### 发布 + +PR 被合并之后,我们会尽快发布相关的正式版本或者 beta 版本。 + +### 加入 Contributor 群 +提交过 Bugfix 或 Feature 类 PR 的同学,如果有兴趣一起参与维护 LowcodeEngine,我们提供了一个核心贡献者交流群。 + +1. 可以通过[填写问卷](https://survey.taobao.com/apps/zhiliao/4YEtu9gHF)的方式,参与到其中。 +2. 填写问卷后加微信号 `wxidvlalalalal` (注明 github id)我们会拉你到群里。 + +如果你不知道可以贡献什么,可以到源码里搜 TODO 或 FIXME 找找。 + +为了使你能够快速上手和熟悉贡献流程,我们这里有个列表 [good first issues](https://github.com/alibaba/lowcode-engine/issues?q=is:open+is:issue+label:%22good+first+issue%22),里面有相对没那么笼统的漏洞,从这开始是个不错的选择。 + +### PR 提交注意事项 + +- lowcode-engine 仓库建议从 develop 创建分支,PR 指向 develop 分支。 +- 其他仓库从 main 分支创建分支,PR 指向 main 分支 +- 如果你修复了 bug 或者添加了代码,而这些内容需要测试,请添加测试! +- 确保通过测试套件(yarn test)。 +- 请签订贡献者许可证协议(Contributor License Agreement)。 + > 如已签署 CLA 仍被提示需要签署,[解决办法](/site/docs/faq/faq021) \ No newline at end of file diff --git a/docs/docs/participate/meet.md b/docs/docs/participate/meet.md new file mode 100644 index 0000000000..23226bf1cd --- /dev/null +++ b/docs/docs/participate/meet.md @@ -0,0 +1,55 @@ +--- +title: 开源社区例会 +sidebar_position: 0 +--- + +## **简介** + +低代码引擎开源社区致力于共同推动低代码技术的发展和创新。本社区汇集了低代码技术领域的开发者、技术专家和行业观察者,通过定期的例会来交流思想、分享经验、讨论新技术,并探索低代码技术的未来发展方向。 + +## 参与要求 + +为了确保例会的质量和效果,我们建议以下人员参加: + +- **已参与低代码引擎贡献的成员**:那些对低代码引擎有实际贡献的社区成员。 +- **参考贡献指南**:可查阅[贡献指南](https://lowcode-engine.cn/site/docs/participate/)获取更多信息。 +- **提供过优秀建议的成员**:那些在过去为低代码引擎提供过有价值建议的成员。 + +## **时间周期** + +- **周期性**:月例会 + +### **特别说明** + +- 例会周期可根据成员反馈进行调整。如果讨论的议题较多,可增加例会频率;若议题较少,单次例会可能取消。若多次取消,可能会暂停例会。 + +## **例会流程** + +### **准备阶段** + +- **定期确定议题**:会前一周确定下一次会议的议题。 +- **分发会议通知**:提前发送会议时间、议程和参与方式。 + +### **会议阶段** + +- **开场和介绍**:简短开场和自我介绍,特别是新成员加入时。 +- **议题讨论**:按照议程进行议题讨论,每个议题分配一定时间,并留足够时间供讨论和提问。 +- **记录要点和决定**:记录讨论要点、决策和任何行动事项。 + +### **后续阶段** + +- **分享会议纪要**:会后将会议纪要和行动计划分发给所有成员。 +- **执行和跟进**:根据会议中的讨论和决策执行相关任务,并在下次会议中进行跟进汇报。 + +## **开源例会议题** + +开源例会议题包括但不限于: + +- **共建低代码行业发展**:探讨通过开源社区合作加速低代码行业发展。 +- **改进建议和反馈收集**:讨论社区成员对低代码引擎的使用体验和改进建议。 +- **前端技术与低代码的结合**:针对前端开发者,讨论将前端技术与低代码引擎结合的方式。 +- **低代码业务场景和经验分享**:邀请社区成员分享低代码引擎的实际应用经验。 +- **低代码技术原理介绍**:深入理解低代码引擎的技术原理和实现方式。 +- **低代码引擎的最新进展**:分享低代码引擎的最新进展,包括新版本发布和新功能实现等。 +- **低代码技术的未来展望**:讨论低代码技术的未来发展方向。 +- **最新低代码平台功能和趋势分析**:分享和讨论当前低代码平台的新功能、趋势和发展方向。 \ No newline at end of file diff --git a/specs/assets-spec.md b/docs/docs/specs/assets-spec.md similarity index 93% rename from specs/assets-spec.md rename to docs/docs/specs/assets-spec.md index ab0e1abc02..5a91b8dde3 100644 --- a/specs/assets-spec.md +++ b/docs/docs/specs/assets-spec.md @@ -1,8 +1,10 @@ -# 《低代码引擎资产包协议规范》 +--- +title: 《低代码引擎资产包协议规范》 +sidebar_position: 2 +--- +## 1 介绍 -# 1 介绍 - -## 1.1 本协议规范涉及的问题域 +### 1.1 本协议规范涉及的问题域 - 定义本协议版本号规范 - 定义本协议中每个子规范需要被支持的 Level @@ -12,16 +14,16 @@ - 定义低代码资产包协议组件描述资源加载规范(A) - 定义低代码资产包协议组件在面板展示规范(AA) -## 1.2 协议草案起草人 +### 1.2 协议草案起草人 - 撰写:金禅、璿玑、彼洋 - 审阅:力皓、絮黎、光弘、戊子、潕量、游鹿 -## 1.3 版本号 +### 1.3 版本号 1.1.0 -## 1.4 协议版本号规范(A) +### 1.4 协议版本号规范(A) 本协议采用语义版本号,版本号格式为 `major.minor.patch` 的形式。 @@ -29,7 +31,7 @@ - minor 是小版本号:用于发布向下兼容的协议功能新增 - patch 是补丁号:用于发布向下兼容的协议问题修正 -## 1.5 协议中子规范 Level 定义 +### 1.5 协议中子规范 Level 定义 | 规范等级 | 实现要求 | | -------- | ------------------------------------------------------------ | @@ -37,19 +39,19 @@ | AA | 推荐规范,由低代码引擎官方插件、setter 支持。 | | AAA | 参考规范,需由基于引擎的上层搭建平台支持,实现可参考该规范。 | -## 1.6 名词术语 +### 1.6 名词术语 - **资产包**: 低代码引擎加载资源的动态数据集合,主要包含组件及其依赖的资源、组件低代码描述、动态插件/设置器资源等。 -## 1.7 背景 +### 1.7 背景 根据低代码引擎的实现,一个组件要在引擎上渲染和配置,需要提供组件的 umd 资源以及组件的`低代码描述`,并且组件通常都是以集合的形式被引擎消费的;除了组件之外,还有组件的依赖资源、引擎的动态插件/设置器等资源也需要注册到引擎中;因此我们定义了“低代码资产包”这个数据结构,来描述引擎所需加载的动态资源的集合。 -## 1.8 受众 +### 1.8 受众 本协议适用于使用“低代码引擎”构建搭建平台的开发者,通过本协议的定义来进行资源的分类和加载。阅读及使用本协议,需要对低代码搭建平台的交互和实现有一定的了解,对前端开发相关技术栈的熟悉也会有帮助,协议中对通用的前端相关术语不会做进一步的解释说明。 -# 2 协议结构 +## 2 协议结构 协议最顶层结构如下,包含 7 方面的描述内容: @@ -61,7 +63,7 @@ - setters { Array } 设计器中设置器描述协议列表 - extConfig { Object } 平台自定义扩展字段 -## 2.1 version(A) +### 2.1 version (A) 定义当前协议 schema 的版本号; @@ -69,9 +71,9 @@ | ---------- | ------ | ---------- | -------- | ------ | | version | String | 协议版本号 | - | 1.1.0 | -## 2.2 packages(A) +### 2.2 packages (A) -定义低代码编辑器中加载的资源列表,包含公共库和组件(库) cdn 资源等; +定义低代码编辑器中加载的资源列表,包含公共库和组件 (库) cdn 资源等; | 字段 | 字段描述 | 字段类型 | 规范等级 | 备注 | | -------------------- | --------------------------------------------------------------- | ------------- | -------- | -------------------------------------------------------------------------------------------------------- | @@ -81,20 +83,20 @@ | packages[].version | npm 包版本号 | String | A | 组件资源版本号 | | packages[].type | 资源包类型 | String | AA | 取值为: proCode(源码)、lowCode(低代码,默认为 proCode | | packages[].schema | 低代码组件 schema 内容 | object | AA | 取值为: proCode(源码)、lowCode(低代码) | -| packages[].deps | 当前资源包的依赖资源的唯一标识列表 | Array | A | 唯一标识为 id 或者 package 对应的值 | +| packages[].deps | 当前资源包的依赖资源的唯一标识列表 | Array | A | 唯一标识为 id 或者 package 对应的值 | | packages[].library | 作为全局变量引用时的名称,用来定义全局变量名 | String | A | 低代码引擎通过该字段获取组件实例 | -| packages[].editUrls | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array | A | 低代码引擎编辑器会加载这些 url | -| packages[].urls | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array | AA | 低代码引擎渲染模块会加载这些 url | +| packages[].editUrls | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array | A | 低代码引擎编辑器会加载这些 url | +| packages[].urls | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array | AA | 低代码引擎渲染模块会加载这些 url | | packages[].advancedEditUrls | 组件多个编辑态视图打包后的 CDN url 列表集合,包含 js 和 css | Object | AAA | 上层平台根据特定标识提取某个编辑态的资源,低代码引擎编辑器会加载这些资源,优先级高于 packages[].editUrls | | packages[].advancedUrls | 组件多个端的渲染态视图打包后的 CDN url 列表集合,包含 js 和 css | Object | AAA | 上层平台根据特定标识提取某个渲染态的资源, 低代码引擎渲染模块会加载这些资源,优先级高于 packages[].urls | | packages[].external | 当前资源在作为其他资源的依赖,在其他依赖打包时时是否被排除了(同 webpack 中 external 概念) | Boolean | AAA | 某些资源会被单独提取出来,是其他依赖的前置依赖,根据这个字段决定是否提前加载该资源 | -| packages[].loadEnv | 指定当前资源加载的环境 | Array | AAA | 主要用于指定 external 资源加载的环境,取值为 design(设计态)、runtime(预览态)中的一个或多个 | +| packages[].loadEnv | 指定当前资源加载的环境 | Array | AAA | 主要用于指定 external 资源加载的环境,取值为 design(设计态)、runtime(预览态) 中的一个或多个 | | packages[].exportSourceId | 标识当前 package 内容是从哪个 package 导出来的 | String | AAA | 此时 urls 无效 | | packages[].exportSourceLibrary | 标识当前 package 是从 window 上的哪个属性导出来的 | String | AAA | exportSourceId 的优先级高于exportSourceLibrary ,此时 urls 无效 | | packages[].async | 标识当前 package 资源加载在 window.library 上的是否是一个异步对象 | Boolean | A | async 为 true 时,需要通过 await 才能拿到真正内容 | -| packages[].exportMode | 标识当前 package 从其他 package 的导出方式 | String | A | 目前只支持 `"functionCall"`, exportMode等于 `"functionCall"` 时,当前package 的内容以函数的方式从其他 package 中导出,具体导出接口如: (library: string, packageName: string, isRuntime?: boolean) => any | Promise, library 为当前 package 的 library, packageName 为当前的包名,返回值为当前 package 的导出内容 | +| packages[].exportMode | 标识当前 package 从其他 package 的导出方式 | String | A | 目前只支持 `"functionCall"`, exportMode等于 `"functionCall"` 时,当前package 的内容以函数的方式从其他 package 中导出,具体导出接口如: (library: string, packageName: string, isRuntime?: boolean) => any | Promise, library 为当前 package 的 library, packageName 为当前的包名,返回值为当前 package 的导出内容 | -描述举例: +描述举例: ```json { @@ -294,14 +296,14 @@ } ``` -## 2.3 components (A) +### 2.3 components (A) 定义资产包中包含的所有组件的低代码描述的集合,分为“ComponentDescription”和“RemoteComponentDescription”(详见 2.6 TypeScript 定义): - ComponentDescription: 符合“组件描述协议”的数据,详见物料规范中`2.2.2 组件描述协议`部分; - RemoteComponentDescription 是将一个或多个 ComponentDescription 构建打包的 js 资源的描述,在浏览器中加载该资源后可获取到其中包含的每个组件的 ComponentDescription 的具体内容; -## 2.4 sort (AA) +### 2.4 sort (AA) 定义组件列表分组 @@ -310,7 +312,7 @@ | sort.groupList | String[] | 组件分组,用于组件面板 tab 展示 | - | ['精选组件', '原子组件'] | | sort.categoryList | String[] | 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列 | - | ['通用', '数据展示', '表格类', '表单类'] | -## 2.5 plugins (AAA) +### 2.5 plugins (AAA) 自定义设计器插件列表 @@ -325,7 +327,7 @@ | plugins[].keywords | String[] | 插件检索关键字 | - | - | | plugins[].reference | Reference | 插件引用的资源包信息 | - | - | -## 2.6 setters (AAA) +### 2.6 setters (AAA) 自定义设置器列表 @@ -340,11 +342,11 @@ | setters[].keywords | String[] | 设置器检索关键字 | - | - | | setters[].reference | Reference | 设置器引用的资源包信息 | - | - | -## 2.7 extConfig (AAA) +### 2.7 extConfig (AAA) -定义平台相关的扩展内容,用于存放平台自身实现的一些私有协议, 以允许存量平台能够平滑地迁移至标准协议。 extConfig 是一个 key-value 结构的对象,协议不会规定 extConfig 中的字段名称以及类型, 完全自定义 +定义平台相关的扩展内容,用于存放平台自身实现的一些私有协议,以允许存量平台能够平滑地迁移至标准协议。extConfig 是一个 key-value 结构的对象,协议不会规定 extConfig 中的字段名称以及类型,完全自定义 -## 2.8 TypeScript 定义 +### 2.8 TypeScript 定义 _组件低代码描述相关部分字段含义详见物料规范中`2.2.2 组件描述协议`部分;_ @@ -463,7 +465,7 @@ export interface Package { */ exportName?: string; /** - * 标识当前 package 资源加载在 window.library 上的是否是一个异步对象 + * 标识当前 package 资源加载在 window.library 上的是否是一个异步对象 */ async?: boolean; /** @@ -471,11 +473,11 @@ export interface Package { */ exportMode?: string; /** - * 标识当前 package 内容是从哪个 package 导出来的 + * 标识当前 package 内容是从哪个 package 导出来的 */ exportSourceId?: string; /** - * 标识当前 package 是从 window 上的哪个属性导出来的 + * 标识当前 package 是从 window 上的哪个属性导出来的 */ exportSourceLibrary?: string; } @@ -684,4 +686,4 @@ export interface ComponentSchema { ``` -`ComponentSchema` 的定义见[低代码业务组件描述](./1.material-spec.md#221-组件规范) +`ComponentSchema` 的定义见[低代码业务组件描述](./material-spec.md#221-组件规范) diff --git a/docs/docs/specs/lowcode-spec.md b/docs/docs/specs/lowcode-spec.md new file mode 100644 index 0000000000..c277214106 --- /dev/null +++ b/docs/docs/specs/lowcode-spec.md @@ -0,0 +1,1653 @@ +--- +title: 《低代码引擎搭建协议规范》 +sidebar_position: 0 +--- + +## 1 介绍 + +### 1.1 本协议规范涉及的问题域 + +- 定义本协议版本号规范 +- 定义本协议中每个子规范需要被支持的 Level +- 定义本协议相关的领域名词 +- 定义搭建基础协议版本号规范(A) +- 定义搭建基础协议组件映射关系规范(A) +- 定义搭建基础协议组件树描述规范(A) +- 定义搭建基础协议国际化多语言支持规范(AA) +- 定义搭建基础协议无障碍访问规范(AAA) + + +### 1.2 协议草案起草人 + +- 撰写:月飞、康为、林熠 +- 审阅:大果、潕量、九神、元彦、戊子、屹凡、金禅、前道、天晟、戊子、游鹿、光弘、力皓 + + +### 1.3 版本号 + +1.1.0 + +### 1.4 协议版本号规范(A) + +本协议采用语义版本号,版本号格式为 `major.minor.patch` 的形式。 + +- major 是大版本号:用于发布不向下兼容的协议格式修改 +- minor 是小版本号:用于发布向下兼容的协议功能新增 +- patch 是补丁号:用于发布向下兼容的协议问题修正 + + +### 1.5 协议中子规范 Level 定义 + +| 规范等级 | 实现要求 | +| -------- | ---------------------------------------------------------------------------------- | +| A | 强制规范,必须实现;违反此类规范的协议描述数据将无法写入物料中心,不支持流通。 | +| AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 | +| AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 | + + +### 1.6 名词术语 + +#### 1.6.1 物料系统名词 + +- **基础组件(Basic Component)**:前端领域通用的基础组件,阿里巴巴前端委员会官方指定的基础组件库是 Fusion Next/AntD。 +- **图表组件(Chart Component)**:前端领域通用的图表组件,有代表性的图表组件库有 BizCharts。 +- **业务组件(Business Component)**:业务领域内基于基础组件之上定义的组件,可能会包含特定业务域的交互或者是业务数据,对外仅暴露可配置的属性,且必须发布到公域(如阿里 NPM);在同一个业务域内可以流通,但不需要确保可以跨业务域复用。 + - **低代码业务组件(Low-Code Business Component)**:通过低代码编辑器搭建而来,有别于源码开发的业务组件,属于业务组件中的一种类型,遵循业务组件的定义;同时低代码业务组件还可以通过低代码编辑器继续多次编辑。 +- **布局组件(Layout Component)**:前端领域通用的用于实现基础组件、图表组件、业务组件之间各类布局关系的组件,如三栏布局组件。 +- **区块(Block)**:通过低代码搭建的方式,将一系列业务组件、布局组件进行嵌套组合而成,不对外提供可配置的属性。可通过 区块容器组的包裹,实现区块内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可通过复制 schema 实现跨页面、跨应用的快速复用,保障功能和数据的正常。 +- **页面(Page)**:由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。 +- **模板(Template)**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。 + + +#### 1.6.2 低代码搭建系统名词 + +- **搭建编辑器**:使用可视化的方式实现页面搭建,支持组件 UI 编排、属性编辑、事件绑定、数据绑定,最终产出符合搭建基础协议规范的数据。 + - **属性面板**:低代码编辑器内部用于组件、区块、页面的属性编辑、事件绑定、数据绑定的操作面板。 + - **画布面板**:低代码编辑器内部用于 UI 编排的操作面板。 + - **大纲面板**:低代码编辑器内部用于页面组件树展示的面板。 +- **编辑器框架**:搭建编辑器的基础框架,包含主题配置机制、插件机制、setter 控件机制、快捷键管理、扩展点管理等底层基础设施。 +- **入料模块**:专注于物料接入,能自动扫描、解析源码组件,并最终产出一份符合《低代码引擎物料协议规范》的 Schema JSON。 +- **编排模块**:专注于 Schema 可视化编排,以可视化的交互方式提供页面结构编排服务,并最终产出一份符合《低代码搭建基础协议规范》的 Schema JSON。 +- **渲染模块**:专注于将 Schema JSON 渲染为 UI 界面,最终呈现一个可交互的页面。 +- **出码模块 Schema2Code**:专注于通过 Schema JSON 生成高质量源代码,将符合《低代码搭建基础协议规范》的 Schema JSON 数据分别转化为面向 React / Rax / 阿里小程序等终端可渲染的代码。 +- **事件绑定**:是指为某个组件的某个事件绑定相关的事件处理动作,比如为某个组件的**点击事件**绑定**一段处理函数**或**响应动作**(比如弹出对话框),每个组件可绑定的事件由该组件自行定义。 +- **数据绑定**:是指为某个组件的某个属性绑定用于该属性使用的数据。 +- **生命周期**: 一般指某个对象的生老病死,本文中指某个实体(组件、容器、区块等等)的创建、加载、显示、销毁等关键生命阶段的统称。 + +### 1.7 背景 + +- **协议目标**:通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。  +- **协议通**: + - 协议顶层结构统一 + - 协议 schema 具备有完整的描述能力,包含版本、国际化、组件树、组件映射关系等; + - 顶层属性 key、value 值的格式,必须保持一致; + - 组件树描述统一 + - 源码组件描述; + - 页面、区块、低代码业务组件这三种容器组件的描述; + - 数据流描述,包含数据请求、数据状态管理、数据绑定描述; + - 事件描述,包含统一事件上下文、统一搭建 API; +- **物料通**:指在相同领域内的不同搭建产品,可直接使用的物料。比如模版、区块、组件; + +### 1.8 受众 + +本协议适用于所有使用低代码搭建平台来开发页面或组件的开发者,以及围绕此协议的相关工具或工程化方案的开发者。阅读及使用本协议,需要对低代码搭建平台的交互和实现有一定的了解,对前端开发相关技术栈的熟悉也会有帮助,协议中对通用的前端相关术语不会做进一步的解释说明。 + +### 1.9 使用范围 + +本协议描述的是低代码搭建平台产物(应用、页面、区块、组件)的 schema 结构,以及实现其数据状态更新(内置 api)、能力扩展、国际化等方面完整,只在低代码搭建场景下可用; + +### 1.10 协议目标 + +一套面向开发者的 schema 规范,用于规范化约束搭建编辑器的输出,以及渲染模块和出码模块的输入,将搭建编辑器、渲染模块、出码模块解耦,保障搭建编辑器、渲染模块、出码模块的独立升级。 + +### 1.11 设计说明 + +- **语义化**:语义清晰,简明易懂,可读性强。 +- **渐进性描述**:搭建的本质是通过 源码组件 进行嵌套组合,从小往大、依次组合生成 组件、区块、页面,最终通过云端构建生成 应用 的过程。因此在搭建基础协议中,我们需要知道如何去渐进性的描述组件、区块、页面、应用这 4 个实体概念。 +- **生成标准源码**:明确每一个属性与源码对应的转换关系,可生成跟手写无差异的高质量标准源代码。 +- **可流通性**:产物能在不同搭建产品中流通,不涉及任何私域数据存储。 +- **面向多端**:不能仅面向 React,还有小程序等多端。 +- **支持国际化&无障碍访问标准的实现** + + +## 2 协议结构 + +协议最顶层结构如下: + +- version { String } 当前协议版本号 +- componentsMap { Array } 组件映射关系 +- componentsTree { Array } 描述模版/页面/区块/低代码业务组件的组件树 +- utils { Array } 工具类扩展映射关系 +- i18n { Object } 国际化语料 +- constants { Object } 应用范围内的全局常量 +- css { string } 应用范围内的全局样式 +- config: { Object } 当前应用配置信息 +- meta: { Object } 当前应用元数据信息 +- dataSource: { Array } 当前应用的公共数据源 +- router: { Object } 当前应用的路由配置信息 +- pages: { Array } 当前应用的所有页面信息 + +描述举例: + +```json +{ + "version": "1.0.0", // 当前协议版本号 + "componentsMap": [{ // 组件描述 + "componentName": "Button", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Select", + "subName": "Button" + }], + "utils": [{ + "name": "clone", + "type": "npm", + "content": { + "package": "lodash", + "version": "0.0.1", + "exportName": "clone", + "subName": "", + "destructuring": false, + "main": "/lib/clone" + } + }, { + "name": "moment", + "type": "npm", + "content": { + "package": "@alifd/next", + "version": "0.0.1", + "exportName": "Moment", + "subName": "", + "destructuring": true, + "main": "" + } + }], + "componentsTree": [{ // 描述内容,值类型 Array + "id": "page1", + "componentName": "Page", // 单个页面,枚举类型 Page|Block|Component + "fileName": "Page1", + "props": {}, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "children": [{ + "componentName": "Div", + "props": { + "className": "" + }, + "children": [{ + "componentName": "Button", + "props": { + "prop1": 1234, // 简单 json 数据 + "prop2": [{ // 简单 json 数据 + "label": "选项 1", + "value": 1 + }, { + "label": "选项 2", + "value": 2 + }], + "prop3": [{ + "name": "myName", + "rule": { + "type": "JSExpression", + "value": "/\w+/i" + } + }], + "valueBind": { // 变量绑定 + "type": "JSExpression", + "value": "this.state.user.name" + }, + "onClick": { // 动作绑定 + "type": "JSFunction", + "value": "function(e) { console.log(e.target.innerText) }" + }, + "onClick2": { // 动作绑定 2 + "type": "JSExpression", + "value": "this.submit" + } + } + }] + }] + }], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.com" + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { // 当前应用配置信息 + "sdkVersion": "1.0.3", // 渲染模块版本 + "historyMode": "hash", // 不推荐,推荐在 router 字段中配置 + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + }, + }, + "theme": { + // for Fusion use dpl defined + "package": "@alife/theme-fusion", + "version": "^0.1.0", + // for Antd use variable + "primary": "#ff9966" + } + }, + "meta": { // 应用元数据信息,key 为业务自定义 + "name": "demo 应用", // 应用中文名称, + "git_group": "appGroup", // 应用对应 git 分组名 + "project_name": "app_demo", // 应用对应 git 的 project 名称 + "description": "这是一个测试应用", // 应用描述 + "spma": "spa23d", // 应用 spm A 位信息 + "creator": "月飞", + "gmt_create": "2020-02-11 00:00:00", // 创建时间 + "gmt_modified": "2020-02-11 00:00:00", // 修改时间 + ... + }, + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "中国" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "China" + } + }, + "router": { + "baseUrl": "/", + "historyMode": "hash", // 浏览器路由:browser 哈希路由:hash + "routes": [ + { + "path": "home", + "page": "page1" + } + ] + }, + "pages": [ + { + "id": "page1", + "treeId": "page1" + } + ] +} +``` + +### 2.1 协议版本号(A) + +定义当前协议 schema 的版本号,不同的版本号对应不同的渲染 SDK,以保障不同版本搭建协议产物的正常渲染; + + +| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 | +| ---------- | ------ | ---------- | -------- | ------ | +| version | String | 协议版本号 | - | 1.0.0 | + + +描述示例: + +```javascript +{ + "version": "1.0.0" +} +``` + +### 2.2 组件映射关系(A) + +协议中用于描述 componentName 到公域组件映射关系的规范。 + + +| 参数 | 说明 | 类型 | 变量支持 | 默认值 | +| --------------- | ---------------------- | ------------------------- | -------- | ------ | +| componentsMap[] | 描述组件映射关系的集合 | **ComponentMap**[] | - | null | + +**ComponentMap 结构描述**如下: + +| 参数 | 说明 | 类型 | 变量支持 | 默认值 | +| ------------- | ------------------------------------------------------------------------------------------------------ | ------- | -------- | ------ | +| componentName | 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 **JS 标识符**,而且是大写字母打头 | String | - | - | +| package | npm 公域的 package name | String | - | - | +| version | package version | String | - | - | +| destructuring | 使用解构方式对模块进行导出 | Boolean | - | - | +| exportName | 包导出的组件名 | String | - | - | +| subName | 下标子组件名称 | String | - | | +| main | 包导出组件入口文件路径 | String | - | - | + + +描述示例: + +```json +{ + "componentsMap": [{ + "componentName": "Button", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true + }, { + "componentName": "MySelect", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Select" + }, { + "componentName": "ButtonGroup", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, { + "componentName": "RadioGroup", + "package": "@alifd/next", + "version": "1.0.0", + "destructuring": true, + "exportName": "Radio", + "subName": "Group" + }, { + "componentName": "CustomCard", + "package": "@ali/custom-card", + "version": "1.0.0" + }, { + "componentName": "CustomInput", + "package": "@ali/custom", + "version": "1.0.0", + "main": "/lib/input", + "destructuring": true, + "exportName": "Input" + }] +} +``` + +出码结果: + +```javascript +// 使用解构方式,destructuring is true. +import { Button } from '@alifd/next'; + +// 使用解构方式,且 exportName 和 componentName 不同 +import { Select as MySelect } from '@alifd/next'; + +// 使用解构方式,并导出其子组件 +import { Button } from '@alifd/next'; +const ButtonGroup = Button.Group; + +import { Radio } from '@alifd/next'; +const RadioGroup = Radio.Group; + +// 不使用解构方式进行导出 +import CustomCard from '@ali/custom-card'; + +// 使用特定路径进行导出 +import { Input as CustomInput } from '@ali/custom/lib/input'; + +``` + + +### 2.3 组件树描述(A) + + +协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由**组件结构**&**容器结构**两种结构嵌套构成。 + +- 组件结构:描述单个组件的名称、属性、子集的结构; +- 容器结构:描述单个容器的数据、自定义方法、生命周期的结构,用于将完整页面进行模块化拆分。 + +与源码对应的转换关系如下: + +- 组件结构:转换成一个 .jsx 文件内 React Class 类 render 函数返回的 **jsx** 代码。 +- 容器结构:将转换成一个标准文件,如 React 的 jsx 文件,export 一个 React Class,包含生命周期定义、自定义方法、事件属性绑定、异步数据请求等。 + +#### 2.3.1 基础结构描述 (A) + +此部分定义了组件结构、容器结构的公共基础字段。 + +> 阅读时可先跳到后续章节,待需要时回来参考阅读 + +##### 2.3.1.1 Props 结构描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ----------- | ------------ | ------ | -------- | ------ | ------------------------------------- | +| id | 组件 ID | String | ✅ | - | 系统属性 | +| className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 | +| style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 | +| ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 | +| extendProps | 组件继承属性 | 变量 | ✅ | - | 仅支持变量绑定,常用于继承属性对象 | +| ... | 组件私有属性 | - | - | - | | + +##### 2.3.1.2 css/less/scss 样式描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | +| ------------- | -------------------------------------------------------------------------- | ------ | -------- | ------ | +| css/less/scss | 用于描述容器组件内部节点的样式,对应生成一个独立的样式文件,不支持 @import | String | - | null | + +描述示例: + +```json +{ + "css": "body {font-size: 12px;} .table { width: 100px; }" +} +``` + +##### 2.3.1.3 ComponentDataSource 对象描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ----------- | ---------------------- | -------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | +| list[] | 数据源列表 | **ComponentDataSourceItem**[] | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) | +| dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function 描述) | + +##### 2.3.1.4 ComponentDataSourceItem 对象描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| id | 数据请求 ID 标识 | String | - | - | | +| isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 | +| isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 | +| type | 数据请求类型 | String | - | fetch | 支持四种类型:fetch/mtop/jsonp/custom | +| shouldFetch | 本次请求是否可以正常请求 | (options: ComponentDataSourceItemOptions) => boolean | - | ```() => true``` | function 参数参考 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | +| willFetch | 单个数据结果请求参数处理函数 | Function | - | options => options | 只接受一个参数(options),返回值作为请求的 options,当处理异常时,使用原 options。也可以返回一个 Promise,resolve 的值作为请求的 options,reject 时,使用原 options | +| requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 | +| dataHandler | request 成功后的回调函数 | Function | - | `response => response.data`| 参数:请求成功后 promise 的 value 值 || +| errorHandler | request 失败后的回调函数 | Function | - | - | 参数:请求出错 promise 的 error 内容 | +| options {} | 请求参数 | **ComponentDataSourceItemOptions**| - | - | 每种请求类型对应不同参数,详见 | 每种请求类型对应不同参数,详见 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | + +**关于 dataHandler 于 errorHandler 的细节说明:** + +request 返回的是一个 promise,dataHandler 和 errorHandler 遵循 Promise 对象的 then 方法,实际使用方式如下: + +```ts +// 伪代码 +try { + const result = await request(fetchConfig).then(dataHandler, errorHandler); + dataSourceItem.data = result; + dataSourceItem.status = 'success'; +} catch (err) { + dataSourceItem.error = err; + dataSourceItem.status = 'error'; +} +``` +**注意:** +- dataHandler 和 errorHandler 只会走其中的一个回调 +- 它们都有修改 promise 状态的机会,意味着可以修改当前数据源最终状态 +- 最后返回的结果会被认为是当前数据源的最终结果,如果被 catch 了,那么会认为数据源请求出错 +- dataHandler 会有默认值,考虑到返回结果入参都是 response 完整对象,默认值会返回 `response.data`,errorHandler 没有默认值 + + +##### 2.3.1.5 ComponentDataSourceItemOptions 对象描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------- | ------------ | ------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | +| uri | 请求地址 | String | ✅ | - | | +| params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) | +| method | 请求方法 | String | ✅ | GET | | +| isCors | 是否支持跨域 | Boolean | ✅ | true | 对应 `credentials = 'include'` | +| timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms | +| headers | 请求头信息 | Object | ✅ | - | 自定义请求头 | + + + +##### 2.3.1.6 ComponentLifeCycles 对象描述 + +生命周期对象,schema 面向多端,不同 DSL 有不同的生命周期方法: + +- React:对于中后台 PC 物料,已明确使用 React 作为最终渲染框架,因此提案采用 [React16 标准生命周期方法](https://reactjs.org/docs/react-component.html)标准来定义生命周期方法,降低理解成本,支持生命周期如下: + - constructor(props, context)  + - 说明:初始化渲染时执行,常用于设置 state 值。 + - render()  + - 说明:执行于容器组件 React Class 的 render 方法最前,常用于计算变量挂载到 this 对象上,供 props 上属性绑定。此 render() 方法不需要设置 return 返回值。 + - componentDidMount() + - 说明:组件已加载 + - componentDidUpdate(prevProps, prevState, snapshot) + - 说明:组件已更新 + - componentWillUnmount() + - 说明:组件即将从 DOM 中移除 + - componentDidCatch(error, info) + - 说明:组件捕获到异常 + +该对象由一系列 key-value 组成,key 为生命周期方法名,value 为 JSFunction 的描述,详见下方示例: + +```json +{ + "componentDidMount": { // key 为上文中 React 的生命周期方法名 + "type": "JSFunction", // type 目前仅支持 JSFunction + "value": "function() {\ // value 为 javascript 函数 + console.log('did mount');\ + }" + }, + "componentWillUnmount": { + "type": "JSFunction", + "value": "function() {\ + console.log('will unmount');\ + }" + } + ... +}, +``` + + +##### 2.3.1.7 dataHandler Function 描述 + +- 参数:为 dataMap 对象,包含字段如下: + - key: 数据 id + - value: 单个请求结果 +- 返回值:数据对象 data,将会在渲染引擎和 schemaToCode 中通过调用 `this.setState(...)` 将返回的数据对象生效到 state 中;支持返回一个 Promise,通过 `resolve(返回数据)`,常用于串行发送请求场景。 + +##### 2.3.1.8 ComponentPropDefinition 对象描述 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------------ | ---------- | -------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------- | +| name | 属性名称 | String | - | - | | +| propType | 属性类型 | String\|Object | - | - | 具体值内容结构,参考《低代码引擎物料协议规范》内的“2.2.2.3 组件属性信息”中描述的**基本类型**和**复合类型** | +| description | 属性描述 | String | - | '' | | +| defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 | + +范例: +```json +{ + "propDefinitions": [{ + "name": "title", + "propType": "string", + "defaultValue": "Default Title" + }, { + "name": "onClick", + "propType": "func" + }] + ... +}, +``` + +#### 2.3.2 组件结构描述(A) + +对应生成源码开发体系中 render 函数返回的 jsx 代码,主要描述有以下属性: + + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| ------------- | ---------------------- | ---------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- | +| id | 组件唯一标识 | String | - | | 可选,组件 id 由引擎随机生成(UUID),并保证唯一性,消费方为上层应用平台,在组件发生移动等场景需保持 id 不变 | +| componentName | 组件名称 | String | - | Div | 必填,首字母大写,同 [componentsMap](#22-组件映射关系 a) 中的要求 | +| props {} | 组件属性对象 | **Props**| - | {} | 必填,详见 | 必填,详见 [Props 结构描述](#2311-props-结构描述) | +| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | +| loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 | +| loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 | +| children | 子组件 | Array | | | 选填,支持变量表达式 | + + +描述举例: + +```json +{ + "componentName": "Button", + "props": { + "className": "btn", + "style": { + "width": 100, + "height": 20 + }, + "text": "submit", + "onClick": { + "type": "JSFunction", + "value": "function(e) {\ + console.log('btn click')\ + }" + } + }, + "condition": { + "type": "JSExpression", + "value": "!!this.state.isshow" + }, + "loop": [], + "loopArgs": ["item", "index"], + "children": [] +} +``` + + +#### 2.3.3 容器结构描述 (A)  + +容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。包含**低代码业务组件容器 Component**、**区块容器 Block**、**页面容器 Page** 3 种。主要描述有以下属性: + +- 组件类型:componentName +- 文件名称:fileName +- 组件属性:props +- state 状态管理:state +- 生命周期 Hook 方法:lifeCycles +- 自定义方法设置:methods +- 异步数据源配置:dataSource +- 条件渲染:condition +- 样式文件:css/less/scss + + +详细描述: + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | +| --------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | +| componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 | +| fileName | 文件名称 | String | - | - | 必填,英文 | +| props { } | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) | +| static | 低代码业务组件类的静态对象 | | | | | +| defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 | +| propDefinitions | 低代码业务组件属性类型定义 | **ComponentPropDefinition**[] | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) | +| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | +| state | 容器初始数据 | Object | ✅ | - | 选填,支持变量表达式 | +| children | 子组件 | Array | - | | 选填,支持变量表达式 | +| css/less/scss | 样式属性 | String | ✅ | - | 选填,详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) | +| lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) | +| methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 | +| dataSource {} | 数据源对象 | **ComponentDataSource**| - | - | 选填,异步数据源,详见 | - | - | 选填,异步数据源,详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) | + + + +#### 完整描述示例 + +描述示例 1:(正常 fetch/mtop/jsonp 请求): + +```json +{ + "componentName": "Block", + "fileName": "block-1", + "props": { + "className": "luna-page", + "style": { + "background": "#dd2727" + } + }, + "children": [{ + "componentName": "Button", + "props": { + "text": { + "type": "JSExpression", + "value": "this.state.btnText" + } + } + }], + "state": { + "btnText": "submit" + }, + "css": "body {font-size: 12px;}", + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() {\ + console.log('did mount');\ + }" + }, + "componentWillUnmount": { + "type": "JSFunction", + "value": "function() {\ + console.log('will unmount');\ + }" + } + }, + "methods": { + "testFunc": { + "type": "JSFunction", + "value": "function() {\ + console.log('test func');\ + }" + } + }, + "dataSource": { + "list": [{ + "id": "list", + "isInit": true, + "type": "fetch/mtop/jsonp", + "options": { + "uri": "", + "params": {}, + "method": "GET", + "isCors": true, + "timeout": 5000, + "headers": {} + }, + "dataHandler": { + "type": "JSFunction", + "value": "function(data, err) {}" + } + }], + "dataHandler": { + "type": "JSFunction", + "value": "function(dataMap) { }" + } + }, + "condition": { + "type": "JSExpression", + "value": "!!this.state.isShow" + } +} +``` + +描述示例 2:(自定义扩展请求处理器类型): + +```json +{ + "componentName": "Block", + "fileName": "block-1", + "props": { + "className": "luna-page", + "style": { + "background": "#dd2727" + } + }, + ... + "dataSource": { + "list": [{ + "id": "list", + "isInit": true, + "type": "custom", + "requestHandler": { + "type": "JSFunction", + "value": "this.utils.hsfHandler" + }, + "options": { + "uri": "hsf://xxx", + "param1": "a", + "param2": "b", + ... + }, + "dataHandler": { + "type": "JSFunction", + "value": "function(data, err) { }" + } + }], + "dataHandler": { + "type": "JSFunction", + "value": "function(dataMap) { }" + } + } +} +``` + +#### 2.3.4 属性值类型描述(A) + +在上述**组件结构**和**容器结构**描述中,每一个属性所对应的值,除了传统的 JS 值类型(String、Number、Object、Array、Boolean)外,还包含有**节点类型**、**事件函数类型**、**变量类型**等多种复杂类型;接下来将对于复杂类型的详细描述方式进行详细介绍。 + +##### 2.3.4.1 节点类型(A) + +通常用于描述组件的某一个属性为 **ReactNode** 或 **Function-Return-ReactNode** 的场景。该类属性的描述均以 **JSSlot** 的方式进行描述,详细描述如下: + +**ReactNode** 描述: + +| 参数 | 说明 | 值类型 | 默认值 | 备注 | +| ----- | ---------- | --------------------- | -------- | -------------------------------------------------------------- | +| type | 值类型描述 | String | 'JSSlot' | 固定值 | +| value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述(A)) | + + +举例描述:如 **Card** 的 **title** 属性 + +```json +{ + "componentName": "Card", + "props": { + "title": { + "type": "JSSlot", + "value": [{ + "componentName": "Icon", + "props": {} + },{ + "componentName": "Text", + "props": {} + }] + }, + ... + } +} + +``` + + +**Function-Return-ReactNode** 描述: + +| 参数 | 说明 | 值类型 | 默认值 | 备注 | +| ------ | ---------- | --------------------- | -------- | -------------------------------------------------------------- | +| type | 值类型描述 | String | 'JSSlot' | 固定值 | +| value | 具体的值 | NodeSchema \| NodeSchema[] | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述 a) | +| params | 函数的参数 | String[] | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 | + + +举例描述:如 **Table.Column** 的 **cell** 属性 + +```json +{ + "componentName": "TabelColumn", + "props": { + "cell": { + "type": "JSSlot", + "params": ["value", "index", "record"], + "value": [{ + "componentName": "Input", + "props": {} + }] + }, + ... + } +} + +``` + +##### 2.3.4.2 事件函数类型(A) + +协议内的事件描述,主要包含**容器结构**的**生命周期**和**自定义方法**,以及**组件结构**的**事件函数类属性**三类。所有事件函数的描述,均以 **JSFunction** 的方式进行描述,保留与原组件属性、生命周期(React / 小程序)一致的输入参数,并给所有事件函数 binding 统一一致的上下文(当前组件所在容器结构的 **this** 对象)。 + +**事件函数类型**的属性值描述如下: + +```json +{ + "type": "JSFunction", + "value": "function onClick(){\ + console.log(123);\ + }" +} +``` + +描述举例: + +```json +{ + "componentName": "Block", + "fileName": "block1", + "props": {}, + "state": { + "name": "lucy" + }, + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() {\ + console.log('did mount');\ + }" + }, + "componentWillUnmount": { + "type": "JSFunction", + "value": "function() {\ + console.log('will unmount');\ + }" + } + }, + "methods": { + "getNum": { + "type": "JSFunction", + "value": "function() {\ + console.log('名称是:' + this.state.name)\ + }" + } + }, + "children": [{ + "componentName": "Button", + "props": { + "text": "按钮", + "onClick": { + "type": "JSFunction", + "value": "function(e) {\ + console.log(e.target.innerText);\ + }" + } + } + }] +} +``` + +##### 2.3.4.3 变量类型(A) + +在上述**组件结构** 或**容器结构**中,有多个属性的值类型是支持变量类型的,通常会通过变量形式来绑定某个数据,所有的变量表达式均通过 JSExpression 表达式,上下文与事件函数描述一致,表达式内通过 **this** 对象获取上下文; + +变量**类型**的属性值描述如下: + + +- return 数字类型 + + ```json + { + "type": "JSExpression", + "value": "this.state.num" + } + ``` +- return 数字类型 + + ```json + { + "type": "JSExpression", + "value": "this.state.num - this.state.num2" + } + ``` +- return "8 万" 字符串类型 + + ```json + { + "type": "JSExpression", + "value": "`${this.state.num}万`" + } + ``` +- return "8 万" 字符串类型 + + ```json + { + "type": "JSExpression", + "value": "this.state.num + '万'" + } + ``` +- return 13 数字类型 + + ```json + { + "type": "JSExpression", + "value": "getNum(this.state.num, this.state.num2)" + } + ``` +- return true 布尔类型 + + ```json + { + "type": "JSExpression", + "value": "this.state.num > this.state.num2" + } + ``` + +描述举例: + +```json +{ + "componentName": "Block", + "fileName": "block1", + "props": {}, + "state": { + "num": 8, + "num2": 5 + }, + "methods": { + "getNum": { + "type": "JSFunction", + "value": "function(a, b){\ + return a + b;\ + }" + } + }, + "children": [{ + "componentName": "Button", + "props": { + "text": { + "type": "JSExpression", + "value": "this.getNum(this.state.num, this.state.num2) + '万'" + } + }, + "condition": { + "type": "JSExpression", + "value": "this.state.num > this.state.num2" + } + }] +} +``` + +##### 2.3.4.4 国际化多语言类型(AA) + +协议内的一些文本值内容,我们希望是和协议全局的国际化多语言语料是关联的,会按照全局国际化语言环境的不同使用对应的语料。所有国际化多语言值均以 **i18n** 结构描述。这样可以更为清晰且结构化得表达使用场景。 + +**国际化多语言类型**的属性值类型描述如下: + +```typescript +type Ti18n = { + type: 'i18n'; + key: string; // i18n 结构中字段的 key 标识符 + params?: Record; // 模版型 i18n 文案的入参,JSDataType 指代传统 JS 值类型 +} +``` + +其中 `key` 对应协议 `i18n` 内容的语料键值,`params` 为语料为字符串模板时的变量内容。 + +假设协议已加入如下 i18n 内容: +```json +{ + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "{name}博士" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "Doctor {name}" + } + } +} +``` + +**国际化多语言类型**简单范例: + +```json +{ + "type": "i18n", + "key": "i18n-jwg27yo4" +} +``` + +**国际化多语言类型**模板范例: + +```json +{ + "type": "i18n", + "key": "i18n-jwg27yo3", + "params": { + "name": "Strange" + } +} +``` + +描述举例: + +```json +{ + "componentName": "Button", + "props": { + "text": { + "type": "i18n", + "key": "i18n-jwg27yo4" + } + } +} +``` + + +#### 2.3.5 上下文 API 描述(A) + +在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器(React Class)的实例化对象,在搭建场景下的渲染模块和出码模块实现上,统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。  + +##### 2.3.5.1 容器 API: + +| 参数 | 说明 | 类型 | 备注 | +| ----------------------------------- | --------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | +| **this {}** | 当前区块容器的实例对象 | Class Instance | - | +| *this*.state | 三种容器实例的数据对象 state | Object | - | +| *this*.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) | +| *this*.customMethod() | 三种容器实例的自定义方法 | Function | - | +| *this*.dataSourceMap {} | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) | +| *this*.reloadDataSource() | 三种容器实例的初始化异步数据请求重载 | Function | 返回 \ | +| **this.page {}** | 当前页面容器的实例对象 | Class Instance | | +| *this.page*.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式;path 路径;uri 页面唯一标识;其它扩展字段 | +| *this.page*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API | +| **this.component {}** | 当前低代码业务组件容器的实例对象 | Class Instance | | +| *this.component*.props | 读取低代码业务组件容器的外部传入的 props | Object | | +| *this.component*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API | +| **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 | +| **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 | + +##### setState + +`setState()` 将对容器 `state` 的更改排入队列,并通知低代码引擎需要使用更新后的 `state` 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式。 + +请将 `setState()` 视为请求而不是立即更新组件的命令。为了更好的感知性能,低代码引擎会延迟调用它,然后通过一次传递更新多个组件。低代码引擎并不会保证 state 的变更会立即生效。 + +`setState()`并不总是立即更新组件,它会批量推迟更新。这使得在调用用 `setState()` 后立即读取 `this.state` 成为了隐患。为了消除隐患,请使用 `setState` 的回调函数(`setState(updater, callback)`),`callback` 将在应用更新后触发。即,如下例所示: + +```js +this.setState(newState, () => { + // 在这里更新已经生效了 + // 可以通过 this.state 拿到更新后的状态 + console.log(this.state); +}); + +// ⚠注意:这里拿到的并不是更新后的状态,这里还是之前的状态 +console.log(this.state); +``` + +如需基于之前的 `state` 来设置当前的 `state`,则可以将传递一个 `updater` 函数:`(state, props) => newState`,例如: + +```js +this.setState((prevState) => ({ count: prevState.count + 1 })); +``` + +为了方便更新部分状态,`setState` 会将 `newState` 浅合并到新的 `state` 上。 + + +##### DataSourceMapItem 结构描述 + +| 参数 | 说明 | 类型 | 备注 | +| ------------ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | +| load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 | +| status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init | +| data | 获取上次请求成功后的数据 | Any | | +| error | 获取上次请求失败的错误对象 | Error 对象 | | + +备注:如果组件没有在区块容器内,而是直接在页面内,那么 `this === this.page` + + +##### 2.3.5.2 循环数据 API + +获取在循环场景下的数据对象。举例:上层组件设置了 loop 循环数据,且设置了 `loopArgs:["item", "index"]`,当前组件的属性表达式或绑定的事件函数中,可以通过 this 上下文获取所在循环的数据环境;默认值为 `['item','index']` ,如有多层循环,需要自定义不同 loopArgs,同样通过 `this[自定义循环别名]` 获取对应的循环数据和序号; + + +| 参数 | 说明 | 类型 | 可选值 | +| ---------- | --------------------------------- | ------ | ------ | +| this.item | 获取当前 index 对应的循环体数据; | Any | - | +| this.index | 当前物料在循环体中的 index | Number | - | + +### 2.4 工具类扩展描述(AA) + +用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。 + +| 参数 | 说明 | 类型 | 支持变量 | 默认值 | +| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | ------ | +| utils[] | 工具类扩展映射关系 | **UtilItem**[] | - | | +| *UtilItem*.name | 工具类扩展项名称 | String | - | | +| *UtilItem*.type | 工具类扩展项类型 | 枚举, `'npm'` (代表公网 npm 类型) / `'tnpm'` (代表阿里巴巴内部 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | | +| *UtilItem*.content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系 a) 或 [JSFunction](#2432事件函数类型 a) | - | | + +描述示例: + +```javascript +{ + utils: [{ + name: 'clone', + type: 'npm', + content: { + package: 'lodash', + version: '0.0.1', + exportName: 'clone', + subName: '', + destructuring: false, + main: '/lib/clone' + } + }, { + name: 'moment', + type: 'npm', + content: { + package: '@alifd/next', + version: '0.0.1', + exportName: 'Moment', + subName: '', + destructuring: true, + main: '' + } + }, { + name: 'recordEvent', + type: 'function', + content: { + type: 'JSFunction', + value: "function(logkey, gmkey, gokey, reqMethod) {\n goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod);\n}" + } + }] +} +``` + +出码结果: + +```javascript +import clone from 'lodash/lib/clone'; +import { Moment } from '@alifd/next'; + +export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { + goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod); +} + +... +``` + +扩展的工具类,用户可以通过统一的上下文 this.utils 方法获取所有扩展的工具类或自定义函数,例如:this.utils.moment、this.utils.clone。搭建协议中的使用方式如下所示: + +```javascript +{ + componentName: 'Div', + props: { + onClick: { + type: 'JSFunction, + value: 'function(){ this.utils.clone(this.state.data); }' + } + } +} +``` + +### 2.5 国际化多语言支持(AA) + +协议中用于描述国际化语料和组件引用国际化语料的规范,遵循集团国际化中台关于国际化语料规范定义。 + + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| ---- | -------------- | ------ | ------ | ------ | +| i18n | 国际化语料信息 | Object | - | null | + + +描述示例: + +```json +{ + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "中国" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "China" + } + } +} +``` + +使用举例: + +```json +{ + "componentName": "Button", + "props": { + "text": { + "type": "i18n", + "key": "i18n-jwg27yo4" + } + } +} +``` + +```json +{ + "componentName": "Button", + "props": { + "text": "按钮", + "onClick": { + "type": "JSFunction", + "value": "function() {\ + console.log(this.i18n('i18n-jwg27yo4'));\ + }" + } + } +} +``` + +使用举例(已废弃) +```json +{ + "componentName": "Button", + "props": { + "text": { + "type": "JSExpression", + "value": "this.i18n['i18n-jwg27yo4']" + } + } +} +``` + +### 2.6 应用范围内的全局常量(AA) + +用于描述在整个应用内通用的全局常量,比如请求 API 的域名、环境等。 + +### 2.7 应用范围内的全局样式(AA) + +用于描述在应用范围内的全局样式,比如 reset.css 等。 + +### 2.8 当前应用配置信息(AA) + +用于描述当前应用的配置信息,比如当前应用的 Shell/Layout、主题等。 + +> 注意:该字段为扩展字段,消费方式由各自场景自己决定,包括运行时和出码。 + +### 2.9 当前应用元数据信息(AA) + +用于描述当前应用的元数据信息,比如当前应用的名称、Git 信息、版本号等等。 + +> 注意:该字段为扩展字段,消费方式由各自场景自己决定,包括运行时和出码。 + +### 2.10 当前应用的公共数据源(AA) + +用于描述当前应用的公共数据源,数据结构跟容器结构里的 ComponentDataSource 保持一致。 +在运行时 / 出码使用时,API 和应用级数据源 API 保持一致,都是 `this.dataSourceMap['globalDSName'].load()` + +### 2.11 当前应用的路由信息(AA) + +用于描述当前应用的路径 - 页面的关系。通过声明路由信息,应用能够在不同的路径里显示对应的页面。 + +##### 2.11.1 Router (应用路由配置)结构描述 + +路由配置的结构说明: + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| ----------- | ---------------------- | ------------------------------- | ------ | --------- | ------ | +| baseName | 应用根路径 | String | - | '/' | 选填| | +| historyMode | history 模式 | 枚举类型,包括'browser'、'hash' | - | 'browser' | 选填| | +| routes | 路由对象组,路径与页面的关系对照组 | Route[] | - | - | 必填| | + + +##### 2.11.2 Route (路由记录)结构描述 + +路由记录,路径与页面的关系对照。Route 的结构说明: + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| -------- | ---------------------------- | ---------------------------- | ------ | ------ | ---------------------------------------------------------------------- | +| name | 该路径项的名称 | String | - | - | 选填 | +| path | 路径 | String | - | - | 必填,路径规则详见下面说明 | +| query | 路径的 query 参数 | Object | - | - | 选填 | +| page | 路径对应的页面 ID | String | - | - | 选填,page 与 redirect 字段中必须要有有一个存在 | +| redirect | 此路径需要重定向到的路由信息 | String \| Object \| Function | - | - | 选填,page 与 redirect 字段中必须要有有一个存在,详见下文 **redirect** | +| meta | 路由元数据 | Object | - | - | 选填 | +| children | 子路由 | Route[] | - | - | 选填 | + +以上结构仅说明了路由记录需要的必需字段,如果需要更多的信息字段可以自行实现。 + +关于 **path** 字段的详细说明: + +路由记录通常通过声明 path 字段来匹配对应的浏览器 URL 来确认是否满足匹配条件,如 `path=abc` 能匹配到 `/abc` 这个 URL。 + +> 在声明 path 字段的时候,可省略 `/`,只声明后面的字符,如 `/abc` 可声明为 `abc`。 + +path(页面路径)是浏览器URL的组成部分,同时大部分网站的 URL 也都受到了 Restful 思想的影响,所以我们也是用类似的形式作为路径的规则基底。 +路径规则是路由配置的重要组成部分,我们希望一个路径配置的基本能力需要支持具体的路径(/xxx)与路径参数 (/:abc)。 + +以一个 `/one/:two?/three/:four?/:five?` 路径为例,它能够解析以下路径: +- `/one/three` +- `/one/:two/three` +- `/one/three/:four` +- `/one/three/:five` +- `/one/:two/three/:four` +- `/one/:two/three/:five` +- `/one/three/:four/:five` +- `/one/:two/three/:four/:five` + +更多的路径规则,如路径中的通配符、多次匹配等能力如有需要可自行实现。 + +关于 **redirect** 字段的详细说明: + +**redirect** 字段有三种填入类型,分别是 `String`、`Object`、`Function`: +1. 字符串(`String`)格式下默认处理为重定向到路径,支持传入 '/xxx'、'/xxx?ab=c'。 +2. 对象(`String`)格式下可传入路由对象,如 { name: 'xxx' }、{ path: '/xxx' },可重定向到对应的路由对象。 +3. 函数`Function`格式为`(to) => Route`,它的入参为当前路由项信息,支持返回一个 Route 对象或者字符串,存在一些特殊情况,在重定向的时候需要对重定向之后的路径进行处理的情况下,需要使用函数声明。 + +```json +{ + "redirect": { + "type": "JSFunction", + "value": "(to) => { return { path: '/a', query: { fromPath: to.path } } }", + } +} +``` + +##### 完整描述示例 + +``` json +{ + "router": { + "baseName": "/", + "historyMode": "hash", + "routes": [ + { + "path": "home", + "page": "home" + }, + { + "path": "/*", + "redirect": "notFound" + } + ] + }, + "componentsTree": [ + { + "id": "home", + ... + }, + { + "id": "notFound", + ... + } + ] +} +``` + +### 2.12 当前应用的页面信息(AA) + +用于描述当前应用的页面信息,比如页面对应的低代码搭建内容、页面标题、页面配置等。 +在一些比较复杂的场景下,允许声明一层页面映射关系,以支持页面声明更多信息与配置,同时能够支持不同类型的产物。 + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| ------- | --------------------- | ------ | ------ | ------ | -------------------------------------------------------- | +| id | 页面 id | String | - | - | 必填 | +| type | 页面类型 | String | - | - | 选填,可用来区分页面的类型 | +| treeId | 对应的低代码树中的 id | String | - | - | 选填,页面对应的 componentsTree 中的子项 id | +| packageId | 对应的资产包对象 | String | - | - | 选填,页面对应的资产包对象,一般用于微应用场景下,当路由匹配到当前页面的时候,会加载 `packageId` 对应的微应用进行渲染。 | +| meta | 页面元信息 | Object | - | - | 选填,用于描述当前应用的配置信息 | +| config | 页面配置 | Object | - | - | 选填,用于描述当前应用的元数据信息 | + + +#### 2.12.1 微应用(低代码+)相关说明 + +在开发过程中,我们经常会遇到一些特殊的情况,比如一个低代码应用想要集成一些别的系统的页面或者系统中的一些页面只能是源码开发(与低代码相对的纯工程代码形式),为了满足更多的使用场景,应用级渲染引擎引入了微应用(微前端)的概念,使低代码页面与其他的页面结合成为可能。 + +微应用对象通过资产包加载,需要暴露两个生命周期方法: +- mount(container: HTMLElement, props: any) + - 说明:微应用挂载到 container(dom 节点)的调用方法,会在渲染微应用时调用 +- unmout(container: HTMLElement, props: any) + - 说明:微应用从容器节点(container)卸载的调用方法,会在卸载微应用时调用 + +> 在微应用的场景下,可能会存在多个页面路由到同一个应用,应用可通过资产包加载,所以需要将对应的页面配置指向对应的微应用(资产包)对象。 + +**描述示例** + +```json +{ + "router": { + "baseName": "/", + "historyMode": "hash", + "routes": [ + { + "path": "home", + "page": "home" + }, + { + "page": "guide", + "page": "guide" + }, + { + "path": "/*", + "redirect": "notFound" + } + ] + }, + "pages": [ + { + "id": "home", + "treeId": "home", + "meta": { + "title": "首页" + } + }, + { + "id": "notFound", + "treeId": "notFound", + "meta": { + "title": "404页面" + } + }, + { + "id": "guide", + "packagId": "microApp" + } + ] +} + +// 资产包 +[ + { + "id": "microApp", + "package": "microApp", + "version": "1.23.0", + "urls": [ + "https://g.alicdn.com/code/lib/microApp.min.css", + "https://g.alicdn.com/code/lib/microApp.min.js" + ], + "library": "microApp" + }, +] +``` + + +## 3 应用描述 + +### 3.1 文件目录 + +以下是推荐的应用目录结构,与标准源码 build-scripts 对齐,这里的目录结构是帮助理解应用级协议的设计,不做强约束 + +```html +├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 +├── public/ # 静态文件,构建时会 copy 到 build/ 目录 +│ ├── index.html # 应用入口 HTML +│ └── favicon.png # Favicon +├── src/ +│ ├── components/ # 应用内的低代码业务组件 +│ │ └── guide-component/ +│ │ ├── index.js # 组件入口 +│ │ ├── components.js # 组件依赖的其他组件 +│ │ ├── schema.js # schema 描述 +│ │ └── index.scss # css 样式 +│ ├── pages/ # 页面 +│ │ └── home/ # Home 页面 +│ │ ├── index.js # 页面入口 +│ │ └── index.scss # css 样式 +│ ├── layouts/ +│ │ └── basic-layout/ # layout 组件名称 +│ │ ├── index.js # layout 入口 +│ │ ├── components.js # layout 组件依赖的其他组件 +│ │ ├── schema.js # layout schema 描述 +│ │ └── index.scss # layout css 样式 +│ ├── config/ # 配置信息 +│ │ ├── components.js # 应用上下文所有组件 +│ │ ├── routes.js # 页面路由列表 +│ │ └── app.js # 应用配置文件 +│ ├── utils/ # 工具库 +│ │ └── index.js # 应用第三方扩展函数 +│ ├── locales/ # [可选] 国际化资源 +│ │ ├── en-US +│ │ └── zh-CN +│ ├── global.scss # 全局样式 +│ └── index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; +├── webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack 配置等 +├── README.md +├── package.json +├── .editorconfig +├── .eslintignore +├── .eslintrc.js +├── .gitignore +├── .stylelintignore +└── .stylelintrc.js +``` + +### 3.2 应用级别 APIs +> 下文中 `xxx` 代指任意 API +#### 3.2.1 路由 Router API + - this.location.`xxx` 「不推荐,推荐统一通过 this.router api」 + - this.history.`xxx` 「不推荐,推荐统一通过 this.router api」 + - this.match.`xxx` 「不推荐,推荐统一通过 this.router api」 + - this.router.`xxx` + +##### Router 结构说明 + +| API | 函数签名 | 说明 | +| -------------- | ---------------------------------------------------------- | -------------------------------------------------------------- | +| getCurrentRoute | () => RouteLocation | 获取当前解析后的路由信息,RouteLocation 结构详见下面说明 | +| push | (target: string \| Route) => void | 路由跳转方法,跳转到指定的路径或者 Route | +| replace | (target: string \| Route) => void | 路由跳转方法,与 `push` 的区别在于不会增加一条历史记录而是替换当前的历史记录 | +| beforeRouteLeave | (guard: (to: RouteLocation, from: RouteLocation) => boolean \| Route) => void | 路由跳转前的守卫方法,详见下面说明 | +| afterRouteChange | (fn: (to: RouteLocation, from: RouteLocation) => void) => void | 路由跳转后的钩子函数,会在每次路由改变后执行 | + +##### 3.2.1.1 RouteLocation(路由信息)结构说明 + +**RouteLocation** 是路由控制器匹配到对应的路由记录后进行解析产生的对象,它的结构如下: + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | 备注 | +| -------------- | ---------------------- | ------ | ------ | ------ | ------ | +| path | 当前解析后的路径 | String | - | - | 必填 | +| hash | 当前路径的 hash 值,以 # 开头 | String | - | - | 必填 | +| href | 当前的全部路径 | String | - | - | 必填 | +| params | 匹配到的路径参数 | Object | - | - | 必填 | +| query | 当前的路径 query 对象 | Object | - | - | 必填,代表当前地址的 search 属性的对象 | +| name | 匹配到的路由记录名 | String | - | - | 选填 | +| meta | 匹配到的路由记录元数据 | Object | - | - | 选填 | +| redirectedFrom | 原本指向向的路由记录 | Route | - | - | 选填,在重定向到当前地址之前,原先想访问的地址 | +| fullPath | 包括 search 和 hash 在内的完整地址 | String | - | - | 选填 | + + +##### beforeRouteLeave +通过 beforeRouteLeave 注册的路由守卫方法会在每次路由跳转前执行。该方法一般会在应用鉴权,路由重定向等场景下使用。 + +> `beforeRouteLeave` 只在 `router.push/replace` 的方法调用时生效。 + +传入守卫的入参为: +* to: 即将要进入的目标路由(RouteLocation) +* from: 当前导航正要离开的路由(RouteLocation) + +该守卫返回一个 `boolean` 或者路由对象来告知路由控制器接下来的行为。 +* 如果返回 `false`, 则停止跳转 +* 如果返回 `true`,则继续跳转 +* 如果返回路由对象,则重定向至对应的路由 + +**使用范例:** + +```json +{ + "componentsTree": [{ + "componentName": "Page", + "fileName": "Page1", + "props": {}, + "children": [{ + "componentName": "Div", + "props": {}, + "children": [{ + "componentName": "Button", + "props": { + "text": "跳转到首页", + "onClick": { + "type": "JSFunction", + "value": "function () { this.router.push('/home'); }" + } + }, + }] + }], + }] +} +``` + + +#### 3.2.2 应用级别的公共函数或第三方扩展 + - this.utils.`xxx` + +#### 3.2.3 国际化相关 API +| API | 函数签名 | 说明 | +| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | +| this.i18n | (i18nKey: string, params?: { [paramName: string]: string; }) => string | i18nKey 是语料的标识符,params 可选,是用来做模版字符串替换的。返回语料字符串 | +| this.getLocale | () => string | 返回当前环境语言 code | +| this.setLocale | (locale: string) => void | 设置当前环境语言 code | + +**使用范例:** +```json +{ + "componentsTree": [{ + "componentName": "Page", + "fileName": "Page1", + "props": {}, + "children": [{ + "componentName": "Div", + "props": {}, + "children": [{ + "componentName": "Button", + "props": { + "children": { + "type": "JSExpression", + "value": "this.i18n('i18n-hello')" + }, + "onClick": { + "type": "JSFunction", + "value": "function () { this.setLocale('en-US'); }" + } + }, + }, { + "componentName": "Button", + "props": { + "children": { + "type": "JSExpression", + "value": "this.i18n('i18n-chicken', { count: this.state.count })" + }, + }, + }] + }], + }], + "i18n": { + "zh-CN": { + "i18n-hello": "你好", + "i18n-chicken": "我有{count}只鸡" + }, + "en-US": { + "i18n-hello": "Hello", + "i18n-chicken": "I have {count} chicken" + } + } +} +``` diff --git a/specs/material-spec.md b/docs/docs/specs/material-spec.md similarity index 82% rename from specs/material-spec.md rename to docs/docs/specs/material-spec.md index 4680338864..c766c68347 100644 --- a/specs/material-spec.md +++ b/docs/docs/specs/material-spec.md @@ -1,8 +1,11 @@ -# 《低代码引擎物料协议规范》 +--- +title: 《低代码引擎物料协议规范》 +sidebar_position: 1 +--- -# 1 介绍 +## 1 介绍 -## 1.1 本协议规范涉及的问题域 +### 1.1 本协议规范涉及的问题域 - 定义本协议版本号规范 - 定义本协议中每个子规范需要被支持的 Level @@ -14,16 +17,16 @@ - 定义中后台物料无障碍访问规范(AAA) -## 1.2 协议草案起草人 +### 1.2 协议草案起草人 - 撰写:九神、大果、元彦、戊子、林熠、屹凡、金禅 - 审阅:潕量、月飞、康为、力皓、荣彬、暁仙、度城、金禅、戊子、林熠、絮黎 -## 1.3 版本号 +### 1.3 版本号 1.0.0 -## 1.4 协议版本号规范(A) +### 1.4 协议版本号规范(A) 本协议采用语义版本号,版本号格式为 `major.minor.patch` 的形式。 @@ -32,7 +35,7 @@ - patch 是补丁号:用于发布向下兼容的协议问题修正 -## 1.5 协议中子规范 Level 定义 +### 1.5 协议中子规范 Level 定义 | 规范等级 | 实现要求 | | -------- | ---------------------------------------------------------------------------------- | @@ -40,26 +43,27 @@ | AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 | | AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 | -## 1.6 名词术语 + +### 1.6 名词术语 - **物料**:能够被沉淀下来直接使用的前端能力,一般表现为业务组件、区块、模板。 - **业务组件(Business Component)**:业务领域内基于基础组件之上定义的组件,可能会包含特定业务域的交互或者是业务数据,对外仅暴露可配置的属性,且必须发布到公域(如阿里 NPM);在同一个业务域内可以流通,但不需要确保可以跨业务域复用。 - **低代码业务组件(Low-Code Business Component)**:通过低代码编辑器搭建而来,有别于源码开发的业务组件,属于业务组件中的一种类型,遵循业务组件的定义;同时低代码业务组件还可以通过低代码编辑器继续多次编辑。 - **区块(Block)**:通过低代码搭建的方式,将一系列业务组件、布局组件进行嵌套组合而成,不对外提供可配置的属性。可通过区块容器组件的包裹,实现区块内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可通过复制 schema 实现跨页面、跨应用的快速复用,保障功能和数据的正常。 - **模板(Template)**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。 -## 1.7 物料规范背景 +### 1.7 物料规范背景 目前集团业务融合频繁,而物料规范的不统一给业务融合带来额外的高成本,另一方面集团各个 BU 的前端物料也存在不同程度的重复建设。我们期望通过集团层面的物料通不阻碍业务融合的发展,同时通过集团层面的物料流通来提升物料丰富度,通过丰富物料的复用来提效中后台系统研发,同时也能给新业务场景提供高质量的启动物料。 -## 1.8 物料规范定义 +### 1.8 物料规范定义 - **源码物料规范**:一套面向开发者的目录规范,用于规范化约束开发过程中的代码、文档、接口规范,以方便物料在集团内的流通。 - **搭建物料规范**:一套面向开发者的 Schema 规范,用于规范化约束开发过程中的代码、文档、接口规范,以方便物料在集团内的流通。 -# 2. 物料规范 - 业务组件规范 +## 2. 物料规范 - 业务组件规范 -## 2.1 源码规范 +### 2.1 源码规范 -### 2.1.1 目录规范(A) +#### 2.1.1 目录规范(A) ``` @@ -80,7 +84,7 @@ component // 组件名称, 比如 biz-button ``` -#### README.md +##### README.md - README.md 应该包含业务组件的源信息、使用说明以及 API,示例如下: @@ -122,7 +126,7 @@ npm install @alifd/ice-layout -S | type | type | String | `primray`、`normal` | normal | ``` -#### package.json +##### package.json `package.json` 中包含了一些依赖信息和配置信息,示例如下: ```json @@ -155,7 +159,7 @@ npm install @alifd/ice-layout -S } ``` -#### src/index.js +##### src/index.js 包含组件的出口文件,示例如下: @@ -174,7 +178,7 @@ export default Button; import Button, { Group } form '@scope/button'; ``` -#### src/index.scss +##### src/index.scss ```css /* 不引入依赖组件的样式,比如组件 import { Button } from '@alifd/next'; */ @@ -189,7 +193,7 @@ import Button, { Group } form '@scope/button'; } ``` -#### demo +##### demo demo 目录存放的是组件的文档,无文档的业务组件无法带来任何价值,因此 demo 是必选项。demo 目录下的文件采取 markdown 的写法,可以是多个文件,示例(demo/basic.md)如下: demo/basic.md @@ -228,16 +232,16 @@ ReactDOM.render(
``` ~~~ -### 2.1.2 API 规范(A) +#### 2.1.2 API 规范(A) API 是组件的属性解释,给开发者作为组件属性配置的参考。为了保持 API 的一致性,我们制定这个 API 命名规范。对于业界通用的,约定俗成的命名,我们遵循社区的约定。对于业界有多种规则难以确定的,我们确定其中一种,大家共同遵守。 -#### 通用规则 +##### 通用规则 - 所有的 API 采用小驼峰的书写规则,如 `onChange`、`direction`、`defaultVisible`。 - 标签名采用大驼峰书写规则,如 `Menu`、`Slider`、`DatePicker`。 -#### 通用命名 +##### 通用命名 | API 名称 | 类型 | 描述 | 常见变量 | | :------------- | :------------- | :----------------------------------------------------------- | :---------------------------------------------------- | @@ -245,7 +249,7 @@ API 是组件的属性解释,给开发者作为组件属性配置的参考。 | direction | enum | 方向,取值采用缩写的方式。 | hoz(水平), ver(垂直) | | align | enum | 对齐方式 | tl, tc, tr, cl, cc, cr, bl, bc, br | | status | enum | 状态 | normal, success, error, warning | -| size | enum | 大小 | small, medium, large 更大或更小可用(xxs, xs, xl, xxl) | +| size | enum | 大小 | small, medium, large 更大或更小可用 (xxs, xs, xl, xxl) | | type | enum or string | 分类:1. dom 结构不变、只有皮肤的变化 2.组件类型只有并列的几类 | normal, primary, secondary | | visible | boolean | 是否显示 | | | defaultVisible | boolean | 是否显示(非受控) | | @@ -257,11 +261,11 @@ API 是组件的属性解释,给开发者作为组件属性配置的参考。 | has+'属性' | boolean | 拥有某个属性 | 例如 `hasArrow`, `hasHeader`, `hasClose` 等等 | -#### 多选枚举 +##### 多选枚举 当某个 API 的接口,允许用户指定多个枚举值的时候,我们把这个接口定义为多选枚举。一个很典型的例子是某个弹层组件的 `closable` 属性,我们会允许:键盘 esc 按键、点击 mask、点击 close 按钮、点击组件以外的任何区域进行关闭。 -不要有一个 API 值,支持多种类型。例如某个弹层的组件,我们会允许 esc、点击 mask、点击 close 按钮等进行关闭。此时 API 设计可以通过多个 API 承载,例如: +不要有一个 API 值,支持多种类型。例如某个弹层的组件,我们会允许 esc、点击 mask、点击 close 按钮等进行关闭。此时 API 设计可以通过多个 API 承载,例如: ```js closable?: boolean; // 默认为 true @@ -276,19 +280,19 @@ true 表示触发规则都会关闭,false 表示触发规则不会关闭。 - ``,任何情况下都不关闭,只能通过受控设置 visible - ``,用户按 esc 或者点击关闭按钮会关闭 -#### 事件 +##### 事件 -- 标准事件或者自定义的符合 w3c 标准的事件,命名必须 on 开头, 即 `on` + 事件名,如 onExpand。 +- 标准事件或者自定义的符合 w3c 标准的事件,命名必须 on 开头, 即 `on` + 事件名,如 onExpand。 -#### 表单规范 +##### 表单规范 - 支持[受控模式](https://reactjs.org/docs/forms.html#controlled-components)(value + onChange) (A) - value 控制组件数据展现 - onChange 组件发生变化时候的回调函数(第一个参数可以给到 value) -- `value={undefined}` 的时候清空数据, field 的 reset 函数会给所有组件下发 undefined 数据 (AA) +- `value={undefined}`的时候清空数据,field 的 reset 函数会给所有组件下发 undefined 数据 (AA)) - 一次完整操作抛一次 onChange 事件 `建议` 比如有 Process 表示进展中的状态,建议增加 API `onProcess`;如果有 Start 表示启动状态,建议增加 API `onStart`  (AA) -#### 属性的传递 +##### 属性的传递 **1. 原子组件(Atomic Component)** > 最小粒子,不能再拆分的组件 @@ -326,7 +330,7 @@ true 表示触发规则都会关闭,false 表示触发规则不会关闭。 **xxxProps 例子**: 比如 `Search` 组件由 `Input` 和 `Button` 构成,`Button` 的属性通过 `buttonProps` 传递给内部的 `Button`。`` -### 2.1.3 入库方式 (A) +#### 2.1.3 入库方式 (A) 入库是指:发布组件,并且存储到集团物料中心,方便统一管理和流通。 @@ -346,13 +350,13 @@ $ iceworks sync ``` -### 2.1.4 国际化多语言支持规范(AA) +#### 2.1.4 国际化多语言支持规范(AA) 文件命名采取 [bcp47](https://tools.ietf.org/html/bcp47) 规范 -#### 目录规范 +##### 目录规范 -在 `src` 目录新增 `locale` 目录用于管理不同语言的文案. +在 `src` 目录新增 `locale` 目录用于管理不同语言的文案。 ``` |- BizHello @@ -363,7 +367,7 @@ $ iceworks sync |------ ja-JP.js ``` -#### 定义不同的语言 +##### 定义不同的语言 ```javascript // zh-CN.js @@ -386,13 +390,13 @@ export default { }; ``` -#### 组件支持多语言建议方案 +##### 组件支持多语言建议方案 ```jsx // index.jsx import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import zh_CN from './locale/zh-CN.js'; // 引入默认语言 +import zhCN from './locale/zh-CN.js'; // 引入默认语言 export default class BizHello extends Component { static componentName = 'BizHello'; @@ -401,7 +405,7 @@ export default class BizHello extends Component { }; static defaultProps = { - locale: zh_CN, + locale: zhCN, }; render() { @@ -413,7 +417,7 @@ export default class BizHello extends Component { } ``` -#### 组件支持全局替换国际化文案 +##### 组件支持全局替换国际化文案 配合 ConfigProvider 支持全局替换国际化文案。 @@ -421,14 +425,14 @@ export default class BizHello extends Component { import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { ConfigProvider } from '@alifd/next'; -import zh_CN from './locale/zh-CN.js'; // 引入默认语言 +import zhCN from './locale/zh-CN.js'; // 引入默认语言 class BizHello extends Component { static propTypes = { locale: PropTypes.object, // 增加 locale,用于配置文案 }; static defaultProps = { - locale: zh_CN, + locale: zhCN, }; render() { @@ -443,11 +447,11 @@ export default ConfigProvider.config(BizHello, { }); ``` -### 2.1.5 主题切换规范(AA) +#### 2.1.5 主题切换规范(AA) 业务组件中如果有自定义的需要跟随主题色的 UI,一定要引入变量的形式,增加组件的流通性。 -#### src/index.scss +##### src/index.scss ```css /* 如果需要引入主题变量引入此段 */ @@ -460,7 +464,7 @@ export default ConfigProvider.config(BizHello, { ``` -### 2.1.6 [Deprecated]支持转设计稿(AAA) +#### 2.1.6 [Deprecated]支持转设计稿(AAA) 对接 sketch 插件(FusionCool)的目的是为了让开发产出的业务组件能够直接给设计师使用,用法类似现在 Fusion Next 基础组件。 @@ -494,12 +498,12 @@ export default { api 属性标准参考 [https://fusion.design/help.html#/dev-biz](https://fusion.design/help.html#/dev-biz) -### 2.1.7 无障碍访问规范(AAA) +#### 2.1.7 无障碍访问规范 (AAA) -无障碍需要符合 [WCAG 2.1 A级标准](https://www.w3.org/TR/WCAG21/),可参考 [W3C 无障碍最佳实践](https://www.w3.org/TR/wai-aria-practices-1.1/)、[Fusion 无障碍指引 2.3.1](https://alibaba-fusion.github.io/next/part1/basics.html) 章节等。 +无障碍需要符合 [WCAG 2.1 A 级标准](https://www.w3.org/TR/WCAG21/),可参考 [W3C 无障碍最佳实践](https://www.w3.org/TR/wai-aria-practices-1.1/)、[Fusion 无障碍指引 2.3.1](https://alibaba-fusion.github.io/next/part1/basics.html) 章节等。 -#### 增加 a11y.md 无障碍 demo +##### 增加 a11y.md 无障碍 demo 必须借助 API 才能完成无障碍工作的组件必须为开发者提供无障碍的使用文档,请[参考](https://fusion.design/pc/component/select?themeid=2#accessibility-container)组件 API 中 `ARIA and Keyboard` ,建议在 `demo` 目录新增 `a11y.md` 文件用于演示组件的无障碍使用。 @@ -513,7 +517,7 @@ component 详细指引查看无障碍开发指南 [https://alibaba-fusion.github.io/next/part1/basics.html](https://alibaba-fusion.github.io/next/part1/basics.html)。 -#### 通过键盘快速访问 +##### 通过键盘快速访问 一般键盘事件有 Up Arrow/Down Arrow/Enter/Esc/Tab @@ -527,7 +531,7 @@ component | Esc | 关闭列表 | -#### 对读屏软件友好 +##### 对读屏软件友好 - 对于组件,我们为开发者内置 `role` 和特定 `aria-_属性`,开发者也可以对非组件 API 属性都可以透传至 DOM 元素,进行修改 `role` 和 `aria-_参数`,但是要注意对应关系,请[参考](https://alibaba-fusion.github.io/next/part1/WAI-ARIA.html)。 - 对一些特殊的组件传递参数才能支持无障碍,设置 `id`,`autoFocus` 和传参数,如下: @@ -536,9 +540,9 @@ component - 传参数 - 有些组件需要根据具体的业务,实现不同的可访问性,这里为开发者内置一些参数,想使用无障碍的时候,用户只需要根据现有的需求,选择对应的内置参数,例如设置 aria-label,以下组件需要用户传参数才支持无障碍组件如下:`NumberPicker`、`Transfer` -## 2.2 低代码规范 +### 2.2 低代码规范 -### 2.2.1 组件规范 +#### 2.2.1 组件规范 通过低代码编辑器搭建而来,有别于源码开发的业务组件,属于业务组件中的一种类型,遵循业务组件的定义;同时低代码业务组件还可以通过低代码编辑器继续多次编辑。 @@ -547,7 +551,7 @@ component | -------------- | ------------------------------------------------------------------------------------------------- | ------ | | version | 协议版本号 | String | | componentsMap | 描述组件映射关系的集合 | Array | -| componentsTree | 低代码业务组件树描述,是长度固定为1的数组, 即数组内仅包含根容器的描述(低代码业务组件容器类型) | Array | +| componentsTree | 低代码业务组件树描述,是长度固定为 1 的数组,即数组内仅包含根容器的描述(低代码业务组件容器类型) | Array | | utils | 工具类扩展映射关系 | Array | | i18n | 国际化语料 | Object | @@ -567,7 +571,7 @@ component "name": "lucy", }, "static": {}, // 用于定义自定组件的 static 属性 - "defaultProps": { // 默认 props: 选填仅用于定义低代码业务组件的默认属性固定对象 + "defaultProps": { // 默认 props:选填仅用于定义低代码业务组件的默认属性固定对象 "name": "xxx" }, "children": [{ @@ -593,10 +597,10 @@ component } ``` -### 2.2.2 组件描述协议 +#### 2.2.2 组件描述协议 对源码组件在低代码搭建平台中使用时所具备的配置能力和交互行为进行规范化描述,让不同平台对组件接入的实现保持一致,让组件针对不同的搭建平台接入时可以使用一份统一的描述内容,让组件在不同的业务中流通成为可能。 -#### 2.2.2.1 协议结构 +##### 2.2.2.1 协议结构 单个组件描述内容为 json 结构,主要包含以下三部分内容,分别为: @@ -604,9 +608,7 @@ component - **组件属性信息 (A):** 描述组件属性信息,通常包含参数、说明、类型、默认值 4 项内容。 - **能力配置/体验增强:** 推荐用于优化搭建产品编辑体验,定制编辑能力的配置信息。 -整体结构概览: [http://lowcode-engine.cn/doc?url=sde3wf](http://lowcode-engine.cn/doc?url=sde3wf) - -#### 2.2.2.2 基础信息(A) +##### 2.2.2.2 基础信息(A) | 字段 | 字段描述 | 字段类型 | 允许空 | | ----------------- | --------------------- | ------------------------- | ------ | @@ -632,7 +634,7 @@ component | priority | 用于描述组件在同一 category 中的排序 | String | 否 | -#### 2.2.2.3 组件属性信息 props (A) +##### 2.2.2.3 组件属性信息 props (A) 描述组件属性信息,通常包含名称、类型、描述、默认值 4 项内容。 @@ -803,7 +805,7 @@ export default class FusionForm extends PureComponent { ``` -#### 2.2.2.4 编辑体验增强 configure +##### 2.2.2.4 编辑体验增强 configure 推荐用于优化搭建产品的编辑体验,定制编辑能力的配置信息,通过能力抽象分类,主要包含如下几个维度的配置项: @@ -817,7 +819,7 @@ export default class FusionForm extends PureComponent { | 【已废弃】experimental (AAA) | 将引擎的一些实验性特性放在这个配置里 | Object | 用户可以提前体验这些特性 | -##### 2.2.2.4.1 属性面板配置 props (A) +###### 2.2.2.4.1 属性面板配置 props (A) props 数组下对象字段描述: @@ -825,23 +827,24 @@ props 数组下对象字段描述: | 字段 | 字段描述 | 字段类型 | 备注 | | ---------- | -------------------------------------------------------------------------------------- | ----------------- | ------------------- | | type | 指定类型 | Enum | 可选值为 `'field' | 'group'` ,默认为 'field'| -| display | 指定类型 | Enum | 可选值为 `'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'` ,默认为 'inline'| +| display | 指定类型 | Enum | 可选值为 `'accordion' \| 'inline' \| 'block' \| 'plain' \| 'popup' \| 'entry'` ,默认为 'inline'| | title | 分类标题 | 属性标题 | String | | | items | 分类下的属性列表 | Array\ | type = 'group' 生效 | | name | 属性名 | String | type = 'field' 生效 | | defaultValue | 默认值 | Any(视字段类型而定) | type = 'field' 生效 | | supportVariable | 是否支持配置变量 | Boolean | type = 'field' 生效 | -| condition | 配置当前 prop 是否展示 | (target: SettingTarget) => boolean; | - | -| setter | 单个控件(setter)描述,搭建基础协议组件的描述对象,支持 JSExpression / JSFunction / JSSlot | `String|Object|Function` | type = 'field' 生效 | +| condition | 配置当前 prop 是否展示 | (target: IPublicModelSettingField) => boolean; | - | +| ignoreDefaultValue | 配置当前 prop 是否忽略默认值处理逻辑,如果返回值是 true 引擎不会处理默认值 | (target: IPublicModelSettingField) => boolean; | - | +| setter | 单个控件 (setter) 描述,搭建基础协议组件的描述对象,支持 JSExpression / JSFunction / JSSlot | `String\|Object\|Function` | type = 'field' 生效 | | extraProps | 其他配置属性(不做流通要求) | Object | 其他配置 | -| extraProps.getValue | setter 渲染时被调用,setter 会根据该函数的返回值设置 setter 当前值 | Function | (target: SettingTarget, value: any) => any; | -| extraProps.setValue | setter 内容修改时调用,开发者可在该函数内部修改节点 schema 或者进行其他操作 | Function | (target: SettingTarget, value: any) => void; | +| extraProps.getValue | setter 渲染时被调用,setter 会根据该函数的返回值设置 setter 当前值 | Function | (target: IPublicModelSettingField, value: any) => any; | +| extraProps.setValue | setter 内容修改时调用,开发者可在该函数内部修改节点 schema 或者进行其他操作 | Function | (target: IPublicModelSettingField, value: any) => void; | -根据属性值类型 propType,确定对应控件类型 (setter) ,详见 [https://lowcode-engine.cn/docV2/grfylu](https://lowcode-engine.cn/docV2/grfylu) +根据属性值类型 propType,确定对应控件类型 (setter) 。 -##### 2.2.2.4.2 通用扩展面板支持性配置 supports (AA) +###### 2.2.2.4.2 通用扩展面板支持性配置 supports (AA) 样式配置面板能力描述,描述是否支持行业样式编辑、是否支持类名设置等。 @@ -864,7 +867,7 @@ props 数组下对象字段描述: ``` -##### 2.2.2.4.3 组件能力配置 component +###### 2.2.2.4.3 组件能力配置 component 与组件相关的能力、约束、行为等描述,有些信息可从组件视图实例上直接获取,包含如下字段: @@ -875,12 +878,12 @@ props 数组下对象字段描述: | isModal(A) | 组件是否带浮层,浮层组件拖入设计器时会遮挡画布区域,此时应当辅助一些交互以防止阻挡 | Boolean | | descriptor(A) | 组件树描述信息 | String | | nestingRule(A) | 嵌套控制:防止错误的节点嵌套,比如 a 嵌套 a, FormField 只能在 Form 容器下,Column 只能在 Table 下等 | Object | -| nestingRule.childWhitelist | 子节点类型白名单 | `String|Function` | -| nestingRule.parentWhitelist | 父节点类型白名单 | `String|Function` | -| nestingRule.descendantBlacklist | 后裔节点类型黑名单 | `String|Function` | -| nestingRule.ancestorWhitelist | 祖父节点类型白名单 | `String|Function` | +| nestingRule.childWhitelist | 子节点类型白名单 | `String\|Function` | +| nestingRule.parentWhitelist | 父节点类型白名单 | `String\|Function` | +| nestingRule.descendantBlacklist | 后裔节点类型黑名单 | `String\|Function` | +| nestingRule.ancestorWhitelist | 祖父节点类型白名单 | `String\|Function` | | isNullNode(AAA) | 是否存在渲染的根节点 | Boolean | -| isLayout(AAA) | 是否是layout布局组件 | Boolean | +| isLayout(AAA) | 是否是 layout 布局组件 | Boolean | | rootSelector(AAA) | 组件选中框的 cssSelector | String | | disableBehaviors(AAA) | 用于屏蔽在设计器中选中组件时提供的操作项,默认操作项有 copy、hide、remove | String[] | | actions(AAA) | 用于详细配置上述操作项的内容 | Object | @@ -913,28 +916,28 @@ props 数组下对象字段描述: } ``` -##### 2.2.2.4.4 高级功能配置 advanced (AAA) +###### 2.2.2.4.4 高级功能配置 advanced (AAA) 组件在低代码引擎设计器中的事件回调和 hooks 等高级功能配置,包含如下字段: | 字段 | 用途 | 类型 | 备注 | | ------------------------------- | --------------------------------------------------------------------------------------------------- | ------- | --- | -|initialChildren |组件拖入“设计器”时根据此配置自动生成 children 节点 schema |NodeData[]/Function NodeData[] | ((target: SettingTarget) => NodeData[]);| -|getResizingHandlers| 用于配置设计器中组件 resize 操作工具的样式和内容| Function| (currentNode: any) => Array<{ type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; content?: ReactElement; propTarget?: string; appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; }> / ReactElement[]; -|callbacks| 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等| Callback| - -|callbacks.onNodeAdd| 在容器中拖入组件时触发的事件回调| Function| (e: MouseEvent, currentNode: any) => any -|callbacks.onNodeRemove| 在容器中删除组件时触发的事件回调| Function| (e: MouseEvent, currentNode: any) => any -|callbacks.onResize| 调整容器尺寸时触发的事件回调,常常与 getResizingHandlers搭配使用| Function| 详见 Types 定义 -|callbacks.onResizeStart| 调整容器尺寸开始时触发的事件回调,常常与 getResizingHandlers搭配使用| Function| 详见 Types 定义 -|callbacks.onResizeEnd| 调整容器尺寸结束时触发的事件回调,常常与 getResizingHandlers搭配使用| Function| 详见 Types 定义 -|callbacks.onSubtreeModified| 容器节点结构树发生变化时触发的回调| Function| (currentNode: any, options: any) => void; -|callbacks.onMouseDownHook| 鼠标按下操作回调| Function| (e: MouseEvent, currentNode: any) => any; -|callbacks.onClickHook| 鼠标单击操作回调| Function| (e: MouseEvent, currentNode: any) => any; -|callbacks.onDblClickHook| 鼠标双击操作回调| Function| (e: MouseEvent, currentNode: any) => any; -|callbacks.onMoveHook| 节点被拖动回调| Function| (currentNode: any) => boolean; -|callbacks.onHoverHook| 节点被 hover 回调| Function| (currentNode: any) => boolean; -|callbacks.onChildMoveHook| 容器节点的子节点被拖动回调| Function| (childNode: any, currentNode: any) => boolean; +|initialChildren | 组件拖入“设计器”时根据此配置自动生成 children 节点 schema |NodeData[]/Function NodeData[] | ((target: IPublicModelSettingField) => NodeData[]);| +|getResizingHandlers| 用于配置设计器中组件 resize 操作工具的样式和内容 | Function| (currentNode: any) => Array<{ type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; content?: ReactElement; propTarget?: string; appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; }> / ReactElement[]; +|callbacks| 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 | Callback| - +|callbacks.onNodeAdd| 在容器中拖入组件时触发的事件回调 | Function| (e: MouseEvent, currentNode: any) => any +|callbacks.onNodeRemove| 在容器中删除组件时触发的事件回调 | Function| (e: MouseEvent, currentNode: any) => any +|callbacks.onResize| 调整容器尺寸时触发的事件回调,常常与 getResizingHandlers 搭配使用 | Function| 详见 Types 定义 +|callbacks.onResizeStart| 调整容器尺寸开始时触发的事件回调,常常与 getResizingHandlers 搭配使用 | Function| 详见 Types 定义 +|callbacks.onResizeEnd| 调整容器尺寸结束时触发的事件回调,常常与 getResizingHandlers 搭配使用 | Function| 详见 Types 定义 +|callbacks.onSubtreeModified| 容器节点结构树发生变化时触发的回调 | Function| (currentNode: any, options: any) => void; +|callbacks.onMouseDownHook| 鼠标按下操作回调 | Function| (e: MouseEvent, currentNode: any) => any; +|callbacks.onClickHook| 鼠标单击操作回调 | Function| (e: MouseEvent, currentNode: any) => any; +|callbacks.onDblClickHook| 鼠标双击操作回调 | Function| (e: MouseEvent, currentNode: any) => any; +|callbacks.onMoveHook| 节点被拖动回调 | Function| (currentNode: any) => boolean; +|callbacks.onHoverHook| 节点被 hover 回调 | Function| (currentNode: any) => boolean; +|callbacks.onChildMoveHook| 容器节点的子节点被拖动回调 | Function| (childNode: any, currentNode: any) => boolean; 描述举例: @@ -962,7 +965,7 @@ props 数组下对象字段描述: } ``` -#### 2.2.2.5 TypeScript 定义 +##### 2.2.2.5 TypeScript 定义 ```TypeScript @@ -1096,7 +1099,7 @@ export interface Advanced { /** * 拖入容器时,自动带入 children 列表 */ - initialChildren?: NodeData[] | ((target: SettingTarget) => NodeData[]); + initialChildren?: NodeData[] | ((target: IPublicModelSettingField) => NodeData[]); /** * @todo 待补充文档 */ @@ -1195,9 +1198,9 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 } ``` -### 2.2.3 资产包协议 +#### 2.2.3 资产包协议 -#### 2.2.3.1 协议结构 +##### 2.2.3.1 协议结构 协议最顶层结构如下,包含 5 方面的描述内容: @@ -1206,7 +1209,7 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 - components { Array } 所有组件的描述协议列表 - sort { Object } 用于描述组件面板中的 tab 和 category -#### 2.2.3.2 version(A) +##### 2.2.3.2 version (A) 定义当前协议 schema 的版本号; @@ -1214,9 +1217,9 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 | ---------- | ------ | ---------- | -------- | ------ | | version | String | 协议版本号 | - | 1.0.0 | -#### 2.2.3.3 packages(A) +##### 2.2.3.3 packages (A) -定义低代码编辑器中加载的资源列表,包含公共库和组件(库) cdn 资源等; +定义低代码编辑器中加载的资源列表,包含公共库和组件 (库) cdn 资源等; | 字段 | 字段描述 | 字段类型 | 备注 | | --------------- | ---------------------- | -------- | ------------------------------------------------------------ | @@ -1224,10 +1227,10 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 | packages[].package (A) | npm 包名 | String | 组件资源唯一标识 | | packages[].version(A) | npm 包版本号 | String | 组件资源版本号 | | packages[].library(A) | 作为全局变量引用时的名称,用来定义全局变量名 | String | 低代码引擎通过该字段获取组件实例 | -| packages[].editUrls (A) | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array | 低代码引擎编辑器会加载这些 url | -| packages[].urls (AA) | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array | 低代码引擎渲染模块会加载这些 url | +| packages[].editUrls (A) | 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css | Array\ | 低代码引擎编辑器会加载这些 url | +| packages[].urls (AA) | 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css | Array\ | 低代码引擎渲染模块会加载这些 url | -描述举例: +描述举例: ```json { @@ -1248,7 +1251,7 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 ] }, { - "title": "fusion组件库", + "title": "fusion 组件库", "package": "@alifd/next", "version": "1.24.18", "urls": [ @@ -1287,11 +1290,11 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 } ``` -#### 2.2.3.4 components (A) +##### 2.2.3.4 components (A) 定义所有组件的描述协议列表,组件描述协议遵循本规范章节 2.2.2 的定义; -#### 2.2.3.5 sort (A) +##### 2.2.3.5 sort (A) 定义组件列表分组 @@ -1300,7 +1303,7 @@ export interface ComponentDescription { // 组件描述协议,通过 npm 中 | sort.groupList | String[] | 组件分组,用于组件面板 tab 展示 | - | ['精选组件', '原子组件'] | | sort.categoryList | String[] | 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列 | - | ['通用', '数据展示', '表格类', '表单类'] | -#### 2.2.3.6 TypeScript 定义 +##### 2.2.3.6 TypeScript 定义 ```TypeScript export interface ComponentSort { @@ -1325,13 +1328,13 @@ export interface RemoteComponentDescription { } ``` -# 3 物料规范-区块规范 +## 3 物料规范 - 区块规范 -## 3.1 源码规范 +### 3.1 源码规范 英文 block, 可复用的代码片段,每个区块对应一个 npm。 -### 3.1.1 目录 (A) +#### 3.1.1 目录 (A) ```html @@ -1351,7 +1354,7 @@ block/ ``` -### 3.1.2 package.json (A) +#### 3.1.2 package.json (A) ```json @@ -1369,9 +1372,9 @@ block/ } ``` -### 3.1.3 html2sketch (3A) +#### 3.1.3 html2sketch (3A) -#### 3.1.3.1 package.json 内 blockConfig 结构 +##### 3.1.3.1 package.json 内 blockConfig 结构 ```json { @@ -1381,14 +1384,14 @@ block/ "category": "form", "screenshot": "https://unpkg.com/@icedesign/user-landing-block/screenshot.png", "views": [{ // 区块视图,配置此项后会进入 fusion cool - "title": "视图1标题", // 区块视图标题 + "title": "视图 1 标题", // 区块视图标题 "props": { // 区块支持的 props "type": "primary" }, "screenshot": "build/views/block_view1.png", // 【编译自动填充】视图截图,会在 build 时自动生成 "html": "build/views/block_view1.html", // 【编译自动填充】视图渲染后 html 结构,会在 build 时自动生成 },{ - "title": "视图2标题", // 区块视图标题 + "title": "视图 2 标题", // 区块视图标题 "props": { // 区块支持的 props "type": "sencondary" }, @@ -1399,7 +1402,7 @@ block/ } ``` -## 3.2 低代码规范 +### 3.2 低代码规范 由业务组件、布局组件进行嵌套组合而成。不对外提供可配置的属性。可通过**区块容器组件**的包裹,实现容器内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可实现跨页面、跨应用的快速复用,保障功能和数据的正常。 @@ -1412,7 +1415,7 @@ block/ | i18n | 国际化语料 | Object | -描述举例1: +描述举例 1: ```json { @@ -1420,7 +1423,7 @@ block/ "componentsMap": [{ }], "componentsTree": [{ // 区块组件树,顶层由区块容器组件包裹; "componentName": "Block", // 区块容器组件名 - "fileName": "block1", // 区块容器1 + "fileName": "block1", // 区块容器 1 "props": {}, "css": "body {font-size: 12px;}", "state": { @@ -1449,7 +1452,7 @@ block/ } ``` -描述举例2: +描述举例 2: ```json { @@ -1478,11 +1481,11 @@ block/ } ``` -# 4 物料规范 - 模板规范 +## 4 物料规范 - 模板规范 -## 4.1 源码规范 +### 4.1 源码规范 -### 4.1.1 目录规范(A) +#### 4.1.1 目录规范(A) 与标准源码 build-scripts 对齐 @@ -1522,13 +1525,13 @@ block/ │ │ └── app.js # 应用配置文件 │ ├── utils/ # 工具库 │ │ └── index.js # 应用第三方扩展函数 -│ ├── stores/ # [可选]全局状态管理 +│ ├── stores/ # [可选] 全局状态管理 │ │ └── user.js -│ ├── locales/ # [可选]国际化资源 +│ ├── locales/ # [可选] 国际化资源 │ │ ├── en-US │ │ └── zh-CN │ ├── global.scss # 全局样式 -│ └── index.jsx # 应用入口脚本, 依赖 config/routes.js 的路由配置动态生成路由; +│ └── index.jsx # 应用入口脚本,依赖 config/routes.js 的路由配置动态生成路由; ├── webpack.config.js # 项目工程配置,包含插件配置及自定义 `webpack` 配置等 ├── README.md ├── package.json @@ -1541,7 +1544,7 @@ block/ ``` -#### 入口文件 +##### 入口文件 (/src/index.jsx) @@ -1557,16 +1560,16 @@ const App = hot(router); ReactDOM.render(, document.getElementById(pkg.config && pkg.config.targetRootID || 'root')); ``` -#### 应用参数配置文件 +##### 应用参数配置文件 (/src/config/app.js) - 支持配置路由方式:historyMode - - 支持2种路由方式: - - 浏览器路由: browser - - 哈希路由:  hash + - 支持 2 种路由方式: + - 浏览器路由:browser + - 哈希路由:hash - 支持透传路由产生的参数到所有组件的上下文 this 对象上 - - history 对象: this.history + - history 对象:this.history - location 对象:this.location - 支持内置 query 参数的解析:this.location.query - match 对象:this.match @@ -1594,7 +1597,7 @@ export default { } ``` -#### 应用扩展配置规范: +##### 应用扩展配置规范: (/src/utils/index.js) @@ -1616,7 +1619,7 @@ export default { } ``` -#### 应用常量配置 +##### 应用常量配置 (/src/config/constants.js) @@ -1626,7 +1629,7 @@ export default { } ``` -#### 应用样式配置 +##### 应用样式配置 (/src/global.scss) @@ -1641,9 +1644,9 @@ a { } ``` -### 4.1.2 html2sketch (AAA) +#### 4.1.2 html2sketch (AAA) -#### 4.1.2.1 package.json 内 scaffoldConfig 结构 +##### 4.1.2.1 package.json 内 scaffoldConfig 结构 ```json { @@ -1653,169 +1656,16 @@ a { "category": "form", "screenshot": "https://unpkg.com/@icedesign/user-landing-block/screenshot.png", "views": [{ // 模板视图,配置此项后会进入 fusion cool - "title": "视图1标题", // 模板视图标题 - "path": "#/dashboard/monitor", // 读取路由列表生成,hash 路由必须加# - "screenshot": "build/views/page0.png", // 【编译自动填充】视图截图,会在 build 时自动生成 - "html": "build/views/page0.html", // 【编译自动填充】视图渲染后 html 结构,会在 build 时自动生成 + "title": "视图 1 标题", // 模板视图标题 + "path": "#/dashboard/monitor", // 读取路由列表生成,hash 路由必须加# + "screenshot": "build/views/page0.png", // 【编译自动填充】视图截图,会在 build 时自动生成 + "html": "build/views/page0.html", // 【编译自动填充】视图渲染后 html 结构,会在 build 时自动生成 },{ - "title": "视图2标题", // 区块视图标题 - "path": "#/dashboard/list", // 读取路由列表生成,hash 路由必须加# - "screenshot": "build/views/page1.png", // 【编译自动填充】视图截图,会在 build 时自动生成 - "html": "build/views/page1.html", // 【编译自动填充】视图渲染后 html 结构,会在 build 时自动生成 - }] - } -} -``` - -## 4.2 低代码规范 - -### 4.2.1 结构描述 - -- version { String } 当前应用协议版本号 -- componentsMap { Array } 当前应用所有组件映射关系 -- componentsTree { Array } 描述应用所有页面、低代码组件的组件树 -- utils { Array } 应用范围内的全局自定义函数或第三方工具类扩展 -- constants { Object } 应用范围内的全局常量 -- css { string } 应用范围内的全局样式 -- config: { Object } 当前应用配置信息 -- meta: { Object } 当前应用元数据信息 -- dataSource: { Array } 当前应用的公共数据源 -- i18n { Object } 国际化语料 - -```json -// 完整应用描述举例: -{ - "version": "1.0.0", // 当前协议版本号 - "componentsMap": [{ // 依赖 npm 组件描述 - "componentName": "Button", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Select", - "subName": "Button" - }], - "componentsTree": [{ // 应用内页面、低代码组件描述 - "componentName": "Page", // 单个页面 - "fileName": "page_index", - "props": {}, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "meta": { // 页面元信息 - "title": "首页", // 页面标题描述 - "router": "/", // 页面路由 - "spmb": "abef21", // spm B 位 - "url": "https://fusion.design", // 页面访问地址 - "creator": "月飞", - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "children": [{ - "componentName": "Div", - "props": { - "className": "red", - }, - "children": [{ - "componentName": "Button", - "props": { - "type": "primary", - "valueBind": { // 变量绑定 - "type": "JSExpression", - "value": "this.state.user.name" - }, - "onClick": { // 动作绑定 - "type": "JSExpression", - "value": "function(e) { console.log(e.target.innerText) }", - } - }, - }] - }, { - "componentName": "Component", // 单个组件 - "fileName": "BasicLayout", // 组件名称 - "props": {}, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "meta": { // 组件元信息 - "title": "导航组件", // 组件中文标题 - "description": "这是一个导航类组件...", // 组件描述 - "creator": "月飞", - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "children": [{ - "componentName": "Nav", - "props": { - "className": "red" - }, - "children": [{ - "componentName": "NavItem", - "props": {} - }] - }] + "title": "视图 2 标题", // 区块视图标题 + "path": "#/dashboard/list", // 读取路由列表生成,hash 路由必须加# + "screenshot": "build/views/page1.png", // 【编译自动填充】视图截图,会在 build 时自动生成 + "html": "build/views/page1.html", // 【编译自动填充】视图渲染后 html 结构,会在 build 时自动生成 }] - }], - "utils": [{ - "name": "clone", - "type": "npm", - "content": { - "package": "lodash", - "version": "0.0.1", - "exportName": "clone", - "subName": "", - "destructuring": false, - "main": "/lib/clone" - } - }, { - "name": "beforeRequestHandler", - "type": "function", - "content": { - "type": "JSFunction", - "value": "function(){\n ... \n}" - } - }], - "constants": { - "ENV": "prod", - "DOMAIN": "xxx.com" - }, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "config": { // 当前应用配置信息 - "sdkVersion": "1.0.3", // 渲染模块版本 - "historyMode": "hash", // 浏览器路由:browser 哈希路由:hash - "targetRootID": "J_Container", - "layout": { - "componentName": "BasicLayout", - "props": { - "logo": "...", - "name": "测试网站" - }, - }, - "theme": { - // for Fusion use dpl defined - "package": "@alife/theme-fusion", - "version": "^0.1.0", - // for Antd use variable - "primary": "#ff9966" - } - }, - "meta": { // 应用元数据信息, key 为业务自定义 - "name": "demo 应用", // 应用中文名称, - "git_group": "appGroup", // 应用对应 git 分组名 - "project_name": "app_demo", // 应用对应 git 的 project 名称 - "description": "这是一个测试应用", // 应用描述 - "spma": "spa23d", // 应用 spma A 位信息 - "creator": "月飞", - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China" - } } } ``` diff --git a/docs/docs/video/index.md b/docs/docs/video/index.md new file mode 100644 index 0000000000..38a0e8a5ce --- /dev/null +++ b/docs/docs/video/index.md @@ -0,0 +1,16 @@ +# 官方视频 +- [2023/11/20 云栖大会|阿里开源低代码引擎及商业解决方案](https://www.bilibili.com/video/BV1Ku4y1w7Zr) +- [2023/08/03 初识低代码引擎](https://www.bilibili.com/video/BV1gu411p7TC) + +# 社区视频 +- [低代码从入门到实战:低代码引擎实践](https://www.bilibili.com/video/BV1aP4y1Q7Xa/) +- [低代码技术在研发团队的应用模式](https://www.bilibili.com/video/BV1L14y1Y72J/) +- [阿里低代码引擎项目实战 (1)-引擎 demo 部署到 faas 服务](https://www.bilibili.com/video/BV1B44y1P7GM/) +- [【有翻车】阿里低代码引擎项目实战 (2)-保存页面到远端存储](https://www.bilibili.com/video/BV1AS4y1K7DP/) +- [阿里巴巴低代码引擎项目实战 (3)-自定义组件接入](https://www.bilibili.com/video/BV1dZ4y1m76S/) +- [阿里低代码引擎项目实战 (4)-自定义插件 - 页面管理](https://www.bilibili.com/video/BV17a411i73f/) +- [阿里低代码引擎项目实战 (4)-用户登录](https://www.bilibili.com/video/BV1Wu411e7EQ/) +- [【有翻车】阿里低代码引擎项目实战 (5)-表单回显](https://www.bilibili.com/video/BV1UY4y1v7D7/) +- [阿里低代码引擎项目实战 (6)-自定义插件 - 页面管理 - 后端](https://www.bilibili.com/video/BV1uZ4y1U7Ly/) +- [阿里低代码引擎项目实战 (6)-自定义插件 - 页面管理 - 前端](https://www.bilibili.com/video/BV1Yq4y1a74P/) +- [阿里低代码引擎项目实战 (7)-自定义插件 - 页面管理 (完结)](https://www.bilibili.com/video/BV13Y4y1e7EV/) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js new file mode 100644 index 0000000000..0aaa4c50f9 --- /dev/null +++ b/docs/docusaurus.config.js @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/no-require-imports */ +// Note: type annotations allow type checking and IDEs autocompletion + +const lightCodeTheme = require('prism-react-renderer/themes/github'); +const darkCodeTheme = require('prism-react-renderer/themes/dracula'); +const navbar = require('./config/navbar'); + +/** @type {import('@docusaurus/types').Config} */ +const config = { + title: 'Low-Code Engine', + tagline: 'Low-Code Engine is awesome!', + url: 'https://lowcode-engine.cn', + baseUrl: '/site/', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', + favicon: + 'https://img.alicdn.com/imgextra/i2/O1CN01TNJDDg20pKniPOkN4_!!6000000006898-2-tps-66-78.png', + + organizationName: 'alibaba', // Usually your GitHub org/user name. + projectName: 'lowcode-engine', // Usually your repo name. + + i18n: { + defaultLocale: 'zh-Hans', + locales: ['zh-Hans'], + }, + + plugins: [ + [ + '@docusaurus/plugin-content-docs', + { + id: 'community', + path: 'community', + routeBasePath: 'community', + sidebarPath: require.resolve('./config/sidebarsCommunity.js'), + }, + ], + ], + + presets: [ + [ + 'classic', + ({ + docs: { + sidebarPath: require.resolve('./config/sidebars.js'), + // lastVersion: 'current', + editUrl: + 'https://github.com/alibaba/lowcode-engine/tree/develop/docs/', + }, + theme: { + customCss: require.resolve('./src/css/custom.css'), + }, + }), + ], + ], + + themeConfig: + ({ + docs: { + sidebar: { + hideable: true, + }, + }, + navbar, + footer: { + // style: 'dark', + copyright: `Copyright © ${new Date().getFullYear()} 阿里巴巴集团, Inc. Built with Docusaurus.`, + }, + // 主题切换 + prism: { + theme: lightCodeTheme, + darkTheme: darkCodeTheme, + }, + // 语雀文档导出的图片,会进行 referrer 校验,这里设置关闭,不然加载不了语雀的图片 + metadata: [{ name: 'referrer', content: 'no-referrer' }], + tableOfContents: { + minHeadingLevel: 2, + maxHeadingLevel: 6, + }, + }), + + themes: [ + [ + require.resolve('@easyops-cn/docusaurus-search-local'), + { + hashed: true, + // For Docs using Chinese, The `language` is recommended to set to: + // ``` + language: ['en', 'zh'], + // ``` + }, + ], + ], +}; + +module.exports = config; diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 0000000000..7facd9db8b --- /dev/null +++ b/docs/package.json @@ -0,0 +1,63 @@ +{ + "name": "@alilc/lowcode-engine-docs", + "version": "1.2.31", + "description": "低代码引擎版本化文档", + "license": "MIT", + "files": [ + "build" + ], + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start --host 0.0.0.0", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc", + "syncOss": "node ./scripts/sync-oss.js" + }, + "dependencies": { + "@docusaurus/core": "^2.2.0", + "@docusaurus/preset-classic": "^2.2.0", + "@easyops-cn/docusaurus-search-local": "^0.32.0", + "@mdx-js/react": "^1.6.22", + "axios": "^1.1.3", + "clsx": "^1.2.1", + "fs-extra": "^10.1.0", + "prism-react-renderer": "^1.3.5", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "^2.2.0", + "@tsconfig/docusaurus": "^1.0.5", + "typescript": "^4.7.4" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "engines": { + "node": ">=16.14" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main" + }, + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" +} diff --git a/docs/scripts/getDocsFromDir.js b/docs/scripts/getDocsFromDir.js new file mode 100644 index 0000000000..18e67e7181 --- /dev/null +++ b/docs/scripts/getDocsFromDir.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); +const matter = require('gray-matter'); + +module.exports = function getDocsFromDir(dir, cateList) { + // docs/ + const baseDir = path.join(__dirname, '../docs/'); + const docsDir = path.join(baseDir, dir); + + function isNil(value) { + return value === undefined || value === null; + } + + function getMarkdownOrder(filepath) { + const { data } = matter(fs.readFileSync(filepath, 'utf-8')); + const { sidebar_position } = data || {}; + return isNil(sidebar_position) ? 100 : sidebar_position; + } + + const docs = glob.sync('*.md?(x)', { + cwd: docsDir, + // ignore: 'README.md', + }); + + const result = docs + .filter((doc) => !/^index.md(x)?$/.test(doc)) + .map((doc) => { + return path.join(docsDir, doc); + }) + .sort((a, b) => { + const orderA = getMarkdownOrder(a); + const orderB = getMarkdownOrder(b); + + return orderA - orderB; + }) + .map((filepath) => { + // /Users/xxx/site/docs/guide/basic/router.md => guide/basic/router + const id = path + .relative(baseDir, filepath) + .replace(/\\/g, '/') + .replace(/\.mdx?/, ''); + return id; + }); + + (cateList || []).forEach((item) => { + const { dir, subCategory, ...otherConfig } = item; + const indexList = glob.sync('index.md?(x)', { + cwd: path.join(baseDir, dir), + }); + if (indexList.length > 0) { + otherConfig.link = { + type: 'doc', + id: `${dir}/index`, + }; + } + result.push({ + type: 'category', + collapsed: false, + ...otherConfig, + items: getDocsFromDir(dir, subCategory), + }); + }); + + return result; +}; diff --git a/docs/scripts/sync-oss.js b/docs/scripts/sync-oss.js new file mode 100644 index 0000000000..2d052dfa12 --- /dev/null +++ b/docs/scripts/sync-oss.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node +const http = require('http'); +const package = require('../package.json'); +const { version, name } = package; +const options = { + method: 'PUT', + hostname: 'uipaas-node.alibaba-inc.com', + path: '/staticAssets/cdn/packages', + headers: { + 'Content-Type': 'application/json', + Cookie: 'locale=en-us', + }, + maxRedirects: 20, +}; + +const onResponse = function (res) { + const chunks = []; + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + const body = Buffer.concat(chunks); + console.table(JSON.stringify(JSON.parse(body.toString()), null, 2)); + }); + + res.on('error', (error) => { + console.error(error); + }); +}; + +const req = http.request(options, onResponse); + +const postData = JSON.stringify({ + packages: [ + { + packageName: name, + version, + }, + ], + // 可以发布指定源的 npm 包,默认公网 npm + useTnpm: true, +}); + +req.write(postData); + +req.end(); diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css new file mode 100644 index 0000000000..9b929baae6 --- /dev/null +++ b/docs/src/css/custom.css @@ -0,0 +1,103 @@ + +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-font-size-base: 14px; + --ifm-color-primary: #0089ff; + --ifm-color-primary-dark: #007be6; + --ifm-color-primary-darker: #0074d9; + --ifm-color-primary-darkest: #0060b3; + --ifm-color-primary-light: #1a95ff; + --ifm-color-primary-lighter: #269bff; + --ifm-color-primary-lightest: #4dacff; + --ifm-code-font-size: 95%; + --ifm-container-width-xl: 2000px; + --aa-search-input-height: 32px; + --ifm-font-family-base: -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, + 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji'; + --ifm-font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace; + --ifm-global-spacing: 1.5rem; + --ifm-line-height-base: 1.85; + /* --ifm-font-color-base: #333; */ +} + +.header-github-link::before { + content: ''; + width: 24px; + height: 24px; + display: flex; + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") + no-repeat; +} + +[data-theme='dark'] .header-github-link::before { + background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") + no-repeat; +} + +.docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.1); + display: block; + margin: 0 calc(-1 * var(--ifm-pre-padding)); + padding: 0 var(--ifm-pre-padding); +} + +html[data-theme='dark'] .docusaurus-highlight-code-line { + background-color: rgba(0, 0, 0, 0.3); +} + +.navbar__logo, +.navbar__search { + margin-right: 2rem; +} + +.hero { + padding: 5rem 0 !important; + box-shadow: var(--ifm-navbar-shadow); +} + +.homepage-content { + max-width: 1400px; + margin: 0 auto; +} + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +.hero__title{ + font-size: 2.4rem; + background: -webkit-linear-gradient(315deg,#0089ff 25%,#30e724); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + display: inline-block; +} + +.pagination-nav__link { + padding: 0.5rem 1.5rem; +} + +.markdown h1:first-child { + --ifm-h1-font-size: 2rem; +} + +.markdown > h2{ + --ifm-h2-font-size: 1.5rem; +} + +.markdown > h3{ + --ifm-h3-font-size: 1.25rem; +} + +.markdown img { + box-shadow: 9px 8px 10px 0px rgb(0 0 0 / 15%); +} diff --git a/docs/src/pages/index-old.tsx b/docs/src/pages/index-old.tsx new file mode 100644 index 0000000000..13be38e6e2 --- /dev/null +++ b/docs/src/pages/index-old.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import clsx from 'clsx'; +import Link from '@docusaurus/Link'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import Layout from '@theme/Layout'; + +import styles from './index.module.css'; + +function HomepageHeader() { + const { siteConfig } = useDocusaurusContext(); + return ( +
+
+

{siteConfig.title}

+

欢迎光临 低代码引擎文档站

+

{siteConfig.tagline}

+
+ + 快速开始 + +
+
+
+ ); +} + +export default function Home(): JSX.Element { + const { siteConfig } = useDocusaurusContext(); + return ( + + + + ); +} diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css new file mode 100644 index 0000000000..ac3d449967 --- /dev/null +++ b/docs/src/pages/index.module.css @@ -0,0 +1,29 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; + height: 60rem; +} + +.heroTitle { + color: #30e724; + font-size: 3rem; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} \ No newline at end of file diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx new file mode 100644 index 0000000000..cdf826a64a --- /dev/null +++ b/docs/src/pages/index.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import BrowserOnly from '@docusaurus/BrowserOnly'; + +export default function ToIndex(): JSX.Element { + return ( + + {() => { + /** + * 跳转到首页 + */ + window.location.href = '/index'; + return <>; + }} + + ); +} diff --git a/docs/src/pages/markdown-page.md b/docs/src/pages/markdown-page.md new file mode 100644 index 0000000000..7d2421c8a6 --- /dev/null +++ b/docs/src/pages/markdown-page.md @@ -0,0 +1,15 @@ + + +# 文档能力介绍 + +这是一个使用 Markdown 编写的任意页面,访问地址为 /markdown-page + +Product Docs Capability Introduction. + +## 功能 + +- ✅ 支持本地离线搜搜 +- ✅ 版本化文档管理 +- ✅ 离线静态部署 diff --git a/docs/static/img/docusaurus.png b/docs/static/img/docusaurus.png new file mode 100644 index 0000000000..f458149e3c Binary files /dev/null and b/docs/static/img/docusaurus.png differ diff --git a/docs/static/img/logo.svg b/docs/static/img/logo.svg new file mode 100644 index 0000000000..9db6d0d066 --- /dev/null +++ b/docs/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/static/img/undraw_docusaurus_mountain.svg b/docs/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000000..af961c49a8 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/undraw_docusaurus_react.svg b/docs/static/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000000..94b5cf08f8 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/static/img/undraw_docusaurus_tree.svg b/docs/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000000..d9161d3392 --- /dev/null +++ b/docs/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/tsconfig.json b/docs/tsconfig.json new file mode 100644 index 0000000000..6f4756980d --- /dev/null +++ b/docs/tsconfig.json @@ -0,0 +1,7 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@tsconfig/docusaurus/tsconfig.json", + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/lerna.json b/lerna.json index dc04c65d56..7fad993f66 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "lerna": "4.0.0", - "version": "1.0.15", + "version": "1.3.2", "npmClient": "yarn", "useWorkspaces": true, "packages": [ diff --git a/modules/code-generator/.gitignore b/modules/code-generator/.gitignore index ec49a49555..bf10c9f823 100644 --- a/modules/code-generator/.gitignore +++ b/modules/code-generator/.gitignore @@ -115,3 +115,8 @@ codealike.json # backup files *.bak +# tests +tests/fixtures/**/actual + + + diff --git a/modules/code-generator/.versionrc b/modules/code-generator/.versionrc new file mode 100644 index 0000000000..a41c11e264 --- /dev/null +++ b/modules/code-generator/.versionrc @@ -0,0 +1,3 @@ +{ + "releaseCommitMessageFormat": "chore(release): code-generator - {{currentTag}}" +} \ No newline at end of file diff --git a/modules/code-generator/CHANGELOG.md b/modules/code-generator/CHANGELOG.md index ce239aba64..62527d0229 100644 --- a/modules/code-generator/CHANGELOG.md +++ b/modules/code-generator/CHANGELOG.md @@ -2,6 +2,161 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.0.7-beta.2](https://github.com/alibaba/lowcode-engine/compare/@alilc/lowcode-code-generator@1.0.3...@alilc/lowcode-code-generator@1.0.7-beta.2) (2022-11-24) + +### Bug Fixes + +* 🐛 解决 react 中 jsx 出码的时候对于循环数据漏包 __$evalArray 的问题 ([3b9b177](https://github.com/alibaba/lowcode-engine/commit/3b9b177b052169cd0c1078cf8b488f04cb374dac)) +* 🐛 解决出码缺乏对于 i18n 数据的 params 的处理的问题 ([2cf788c](https://github.com/alibaba/lowcode-engine/commit/2cf788c1716ae63fef20004348c59a5a65c6b3d2)), closes [#288](https://github.com/alibaba/lowcode-engine/issues/288) +* 🐛 解决小程序环境没有 window, 而 rax 出码中却默认在 __$eval 中用到 window 的问题 ([ce531ae](https://github.com/alibaba/lowcode-engine/commit/ce531aeb457711fac92d828b431cfc3d643b3682)) +* add support for jsx expression ([453e069](https://github.com/alibaba/lowcode-engine/commit/453e0699ece06d98e59227e23248baf1de4082aa)) +* 修复生成的 icejs 项目不支持 constants 的问题, fixes [#1259](https://github.com/alibaba/lowcode-engine/issues/1259) ([a079fbc](https://github.com/alibaba/lowcode-engine/commit/a079fbc256f8275e8a69eb6d8abb6f6b08179578)) +* 修正 react 框架出码中在严格模式对 methods 和 context 的处理 ([b1a6100](https://github.com/alibaba/lowcode-engine/commit/b1a61006bba4292790899c7c49c9c611a9384472)) +### [1.0.7-beta.1](https://github.com/alibaba/lowcode-engine/compare/@alilc/lowcode-code-generator@1.0.7-beta.0...@alilc/lowcode-code-generator@1.0.7-beta.1) (2022-10-26) + + +### Bug Fixes + +* fix empty string lost when generate variable ([2cf74cd](https://github.com/alibaba/lowcode-engine/commit/2cf74cd04b4f48a3501d37329d39784f6964366a)) + +### [1.0.7-beta.0](https://github.com/alibaba/lowcode-engine/compare/@alilc/lowcode-code-generator@1.0.6-beta.0...@alilc/lowcode-code-generator@1.0.7-beta.0) (2022-10-25) + +### [1.0.6-beta.0](https://github.com/alibaba/lowcode-engine/compare/@alilc/lowcode-code-generator@1.0.3...@alilc/lowcode-code-generator@1.0.6-beta.0) (2022-10-25) + + +### Features + +* 🎸 设计态支持数据源引擎配置 ([04631f8](https://github.com/alibaba/lowcode-engine/commit/04631f813782dbf6d175f51c40ccc75ca4c099d2)) +* 大纲树支持节点过滤 ([f30db20](https://github.com/alibaba/lowcode-engine/commit/f30db20606f5f2fdac0017305b1dda7ab2258c4b)) +* 为 renderer 追加 displayName,以支持后续的反射功能 ([6399cce](https://github.com/alibaba/lowcode-engine/commit/6399cce05ae494dac6facf4366949b0b97576079)) +* 资产包支持一个package从另一个package异步导出 ([#1150](https://github.com/alibaba/lowcode-engine/issues/1150)) ([6b78157](https://github.com/alibaba/lowcode-engine/commit/6b78157b211d6eabf60297b9ce980a3e10cc8272)) +* add availableActions for ComponentMeta ([de1f60b](https://github.com/alibaba/lowcode-engine/commit/de1f60bbee157267e2c2212df1077cc49ce506f4)) +* add code coverage action ([ed3ddcf](https://github.com/alibaba/lowcode-engine/commit/ed3ddcf5c942a8e78e1f247e41d4159da97e75a8)) +* add componentMeta getter for setingPropEntry ([2f8b954](https://github.com/alibaba/lowcode-engine/commit/2f8b9545de0210260001a832b52f608238ac4191)) +* add expanded to shell SettingPropEntry ([534e294](https://github.com/alibaba/lowcode-engine/commit/534e29429d445d97c71d95d4c4e492868527bb6b)) +* add flag createIfNode for ShellNode#getProp ([152a24d](https://github.com/alibaba/lowcode-engine/commit/152a24d65528d0a3b7990c9aa87e6d8d09aa9b2a)) +* add getComponentsMap() for DocumentModel ([f956645](https://github.com/alibaba/lowcode-engine/commit/f9566454ef83eb4c48b68d63a766c3d0ff927c73)) +* add getExtraPropValue setExtraPropValue to shell SettingPropEntry ([70e7c1c](https://github.com/alibaba/lowcode-engine/commit/70e7c1c2e8998e80d58447759efdf651105724a9)) +* add id setter for DocumentModel ([941ae05](https://github.com/alibaba/lowcode-engine/commit/941ae0592586334694c48197aaa6692d49cefbce)) +* add importSchema event for documentModel ([4b8ec09](https://github.com/alibaba/lowcode-engine/commit/4b8ec09e86e3950a9d4066c28e681a59273b4c93)) +* add isGroup & items to shell SettingPropEntry ([7b76ff3](https://github.com/alibaba/lowcode-engine/commit/7b76ff357e4e638454c31a9b1324fb68966ec522)) +* add mergeChldren API for shell node ([a47d4ee](https://github.com/alibaba/lowcode-engine/commit/a47d4eea28cf4479e3b3a2bd1d194a6433666825)) +* add onMountNode event for DocumentModel ([dcc247c](https://github.com/alibaba/lowcode-engine/commit/dcc247c7d54f6af2ed36d46bfd79c7eacf7bd604)) +* add renderer for simulatorHost ([1cfc8d6](https://github.com/alibaba/lowcode-engine/commit/1cfc8d668b8897ef3a53c11520312cc6d18338f9)) +* add script for synchronizing packages to intranet registry ([b4f463e](https://github.com/alibaba/lowcode-engine/commit/b4f463e7d45f7b476de04bd4d98ad9f74d53cf13)) +* add scrollToNode for simulator host ([#1075](https://github.com/alibaba/lowcode-engine/issues/1075)) ([0bcd9ff](https://github.com/alibaba/lowcode-engine/commit/0bcd9ff78227aeddaf2fdc22d10fbd662fed91d3)) +* add setVisible for Node ([ba90327](https://github.com/alibaba/lowcode-engine/commit/ba90327eac0f5f82f6349bb9a7684bf51259e9c9)) +* add showArea & hideArea method for skeleton ([8f6b53e](https://github.com/alibaba/lowcode-engine/commit/8f6b53e67d89ee7af754132f0994a759522b3821)) +* add slotNode for shell prop ([d9a44c5](https://github.com/alibaba/lowcode-engine/commit/d9a44c5de7861e9180567b4afb787e381cefac61)) +* add some features ([18d1a4f](https://github.com/alibaba/lowcode-engine/commit/18d1a4fe1d952bcd4715e693def09fee94da49a5)) +* add some necessary methods and attributes ([4fd7f27](https://github.com/alibaba/lowcode-engine/commit/4fd7f27f8eb33b66324ede279b412940fc1f7032)) +* add some params for onDragstart & onDrag & onDragend ([d1c9838](https://github.com/alibaba/lowcode-engine/commit/d1c9838343ba1bdd4c02c1cfbf1f920dd8c87e7d)) +* add top attrbute for Shell SettingPropEntry ([51aca3d](https://github.com/alibaba/lowcode-engine/commit/51aca3d330b6483c05b71867f1b362a9f5db6cfe)) +* added lowcode engine standard specs ([f25feba](https://github.com/alibaba/lowcode-engine/commit/f25feba63f181efa83f1a8dff530e1c39ab1b34c)) +* added lowcode engine standard specs ([57df803](https://github.com/alibaba/lowcode-engine/commit/57df803179ca9cec4e8ab1dac1be577175732e65)) +* added thisRequiredInJSE API to control whether JSExpression expression access context must use this ([#702](https://github.com/alibaba/lowcode-engine/issues/702)) ([da7f77e](https://github.com/alibaba/lowcode-engine/commit/da7f77ee91b3bf441a4a57614872df32d6a1d041)) +* assetLoader loda scripts with async=false ([f6ad4a1](https://github.com/alibaba/lowcode-engine/commit/f6ad4a157df8c0ff7db327f4770f454998693d9a)) +* change loop sertter config, set defaultValue prop to JsonSetter ([aa6b9c8](https://github.com/alibaba/lowcode-engine/commit/aa6b9c8f7a5353771af9f46216310f044e57c533)) +* cp dist files of simulator-renderer to that of engine ([03c5397](https://github.com/alibaba/lowcode-engine/commit/03c53971df6de8c35620fd77743ac4f57a82d323)) +* export nodeChildrenSymbol && remove some unnecessary editor.set ([e83adce](https://github.com/alibaba/lowcode-engine/commit/e83adcee815eea73b6b1ed4f43f4d684c11818ca)) +* fix render-core leaf hoc component condition config should get from leaf exportSchema fn ([85704c3](https://github.com/alibaba/lowcode-engine/commit/85704c36946191a1b88db789cfac59e9d027a371)) +* low-code components support lifecycle and function execution ([176583f](https://github.com/alibaba/lowcode-engine/commit/176583f48af573d30c0d2c36faa3d901b0541c06)) +* **material-parser:** check module before install it; fix default value issue in ts parser ([fc452f7](https://github.com/alibaba/lowcode-engine/commit/fc452f7166f02acfba6076c1a9425e6f5880b5f6)) +* modify the output method of rendering module parsing errors ([8255b79](https://github.com/alibaba/lowcode-engine/commit/8255b7945836ee5d25fae73913faa6d0af7b3ff3)) +* pass e to customizeIgnoreSelectors ([900b239](https://github.com/alibaba/lowcode-engine/commit/900b2394323e85f0dce5df83dfc773f96da23e24)) +* refine nesting drawer ([4c032d0](https://github.com/alibaba/lowcode-engine/commit/4c032d0d0ead9731c038bd62dccc4a7d96435183)) +* refine nesting drawer ([94a211e](https://github.com/alibaba/lowcode-engine/commit/94a211e2795f74721cfd2ae3ff38a1d3607e9cd0)) +* refine pop drawer ([abf8fae](https://github.com/alibaba/lowcode-engine/commit/abf8fae3ef4d62b5688362e1b98f1b508a207029)) +* requestHandlersMap should be optional ([ee7160e](https://github.com/alibaba/lowcode-engine/commit/ee7160ea3c625d421c07730ef51711b8f14392a0)) +* return unbind function for onChangeDetecting & onChangeSelection ([30267cb](https://github.com/alibaba/lowcode-engine/commit/30267cb173fca2cd80a61450b9f2fe2bceac0f06)) +* support for hiding settings tabs when there is only one item ([#669](https://github.com/alibaba/lowcode-engine/issues/669)) ([cbd95a1](https://github.com/alibaba/lowcode-engine/commit/cbd95a1778415406670f37507ce957af6b3ecd4a)) +* support for NotFoundComponent design state is optional ([#1013](https://github.com/alibaba/lowcode-engine/issues/1013)) ([d3c891e](https://github.com/alibaba/lowcode-engine/commit/d3c891e2a46d138e31c81a7f9b804a8240154df5)) +* support opening document with id ([3f7c0cd](https://github.com/alibaba/lowcode-engine/commit/3f7c0cd5191b7924f2630c58e6439f4d4a936ac9)) +* support SPA mode ([1f9150e](https://github.com/alibaba/lowcode-engine/commit/1f9150e4b260d522bd7cb31497069b700a1e8576)) +* sync utils/constants ([#506](https://github.com/alibaba/lowcode-engine/issues/506)) ([2871b5b](https://github.com/alibaba/lowcode-engine/commit/2871b5ba4c3dbf1ed76bf4d6359fb457190a9b22)) +* the tips when dragging a component from the component panel same with the moving component ([dbe0764](https://github.com/alibaba/lowcode-engine/commit/dbe0764ff4901450f03ca56b62167fbc87d2524a)) + + +### Bug Fixes + +* 🐛 解决 react 中 jsx 出码的时候对于循环数据漏包 __$evalArray 的问题 ([3b9b177](https://github.com/alibaba/lowcode-engine/commit/3b9b177b052169cd0c1078cf8b488f04cb374dac)) +* 🐛 解决出码缺乏对于 i18n 数据的 params 的处理的问题 ([2cf788c](https://github.com/alibaba/lowcode-engine/commit/2cf788c1716ae63fef20004348c59a5a65c6b3d2)), closes [#288](https://github.com/alibaba/lowcode-engine/issues/288) +* 🐛 解决小程序环境没有 window, 而 rax 出码中却默认在 __$eval 中用到 window 的问题 ([ce531ae](https://github.com/alibaba/lowcode-engine/commit/ce531aeb457711fac92d828b431cfc3d643b3682)) +* 🐛 修复数据源引擎请求处理器映射严格模式下被过滤的问题 ([75626d8](https://github.com/alibaba/lowcode-engine/commit/75626d877db017b8862b1d5e64d75f3af7ff667a)) +* 🐛 修正 i18n 里面的一个参数命名问题 ([20c6fca](https://github.com/alibaba/lowcode-engine/commit/20c6fca03e99b11fa5c257cbbda0d4d23f410090)) +* 新元素无法在大纲树拖拽 ([3d41fd5](https://github.com/alibaba/lowcode-engine/commit/3d41fd5d0783048a7cfb54c6f80d058856153d25)) +* 修复React17选中组件bug ([750d282](https://github.com/alibaba/lowcode-engine/commit/750d282c03a880204fefdef01e180510465b82f8)) +* 修正 react 框架出码中在严格模式对 methods 和 context 的处理 ([b1a6100](https://github.com/alibaba/lowcode-engine/commit/b1a61006bba4292790899c7c49c9c611a9384472)) +* 左侧抽屉固定模式层级不足 ([c657cee](https://github.com/alibaba/lowcode-engine/commit/c657cee0694e3126dee89588a2aa17c4e118f786)) +* add lowcode-designer, lowcode-utils dependencies ([d250242](https://github.com/alibaba/lowcode-engine/commit/d2502427ca988881747a35bd8da49f024939b833)) +* add support for jsx expression ([1043ef8](https://github.com/alibaba/lowcode-engine/commit/1043ef82b1e9ceefc3b74fd21eb28e9a740bd1db)) +* addon-combine affect metadata unexpectedly ([fc5fbc6](https://github.com/alibaba/lowcode-engine/commit/fc5fbc63a04a32bc887754f32e74c76149d74b05)) +* adjust synchronize-order of packages ([81a7304](https://github.com/alibaba/lowcode-engine/commit/81a73049bd848524e1156761ded08ddf325863ba)) +* change typescript type export to export type ([50e4a03](https://github.com/alibaba/lowcode-engine/commit/50e4a03b7d810131ce413cc057b43d4a726f1ebe)) +* change typescript type export to export type ([573504b](https://github.com/alibaba/lowcode-engine/commit/573504b0e3537ca60d234ce2b2f3feedb323405e)) +* declare parameter appHelper for valid engine options ([058a842](https://github.com/alibaba/lowcode-engine/commit/058a84226af8ca19d8c7d63599d80d0cdf70281c)) +* defaultValue should be evaluated inspite of condition result is falsy, fixes [#1045](https://github.com/alibaba/lowcode-engine/issues/1045) ([fcfce3c](https://github.com/alibaba/lowcode-engine/commit/fcfce3cbeb5a53600c40aea07ffef19c9c9591c4)) +* delete the defaultValue configuration outside the loop ([acf7449](https://github.com/alibaba/lowcode-engine/commit/acf7449ca231d45e8ed7e1d9416817ea11b1266f)) +* delete unused typescript types ([63f5d2c](https://github.com/alibaba/lowcode-engine/commit/63f5d2ca3e0bda92898fd0df28c9500707812082)) +* delete unused typescript types ([2432aed](https://github.com/alibaba/lowcode-engine/commit/2432aed83d55407d2f8b5f94910ada7ea78bb59e)) +* designer/loadIncrementalAssets await Sequential ([#841](https://github.com/alibaba/lowcode-engine/issues/841)) ([8232424](https://github.com/alibaba/lowcode-engine/commit/823242469743d235923b3b946ec7d2db70887ead)) +* error thrown when triggering undo after save schema on SchemaPane ([9be46e7](https://github.com/alibaba/lowcode-engine/commit/9be46e7b34e3a40cbc489dbae4bfd0915c2090e3)) +* fallback focusNode to root if empty ([a9a118f](https://github.com/alibaba/lowcode-engine/commit/a9a118fe6e79080245c6eea42ed26772b7c784ca)) +* **filter:** unique key prop warning ([3fe6e41](https://github.com/alibaba/lowcode-engine/commit/3fe6e41536cd3a9b9c7eaca5b353de4bd1f91b11)) +* **filter:** unique key prop warning ([06e6920](https://github.com/alibaba/lowcode-engine/commit/06e6920602bdf21b6e1ffe5cfa3dfe4856e7c57e)) +* fix css resources with parameters not loading correctly ([f859752](https://github.com/alibaba/lowcode-engine/commit/f85975211814147d40ae5330a76cb21cb6c66916)) +* fix css resources with parameters not loading correctly ([9a5a04a](https://github.com/alibaba/lowcode-engine/commit/9a5a04ac9560fb6a51bf4e0ed8ea446381d39c35)) +* fix dataSource needs to be compatible due to empty schema ([98bc477](https://github.com/alibaba/lowcode-engine/commit/98bc477d80dbf7993f89befdb42762d78a55fb1b)) +* fix displayName spell mistake ([2b2bcbd](https://github.com/alibaba/lowcode-engine/commit/2b2bcbdaebde6a3ce974072f586386ef7ef3497c)) +* fix internal project.getSchema default stage is error ([0d40db2](https://github.com/alibaba/lowcode-engine/commit/0d40db2581f4fe5a9e22f763f21aec641e366c34)) +* fix lint issues for renderer-core/renderer/base ([d85437d](https://github.com/alibaba/lowcode-engine/commit/d85437d4af1043371e27dfde98cecf914b93a126)) +* fix lint issues for renderer-core/renderer/base ([4b59190](https://github.com/alibaba/lowcode-engine/commit/4b59190c7f9d518bc7efac44b7eeee73f1b5d177)) +* fix low-code component rendering problems: 1. thisRequiredInJSE does not take effect 2. jsx components cannot obtain source components ([5dd4625](https://github.com/alibaba/lowcode-engine/commit/5dd462544fbbbccfa97165f2bcfeed8629fab2a3)) +* fix material-spec demo ([438cccd](https://github.com/alibaba/lowcode-engine/commit/438cccd58e4341638070c1d8b2d4e78e4e20e3fb)) +* fix misused doc urls ([16a8857](https://github.com/alibaba/lowcode-engine/commit/16a88578634b9da2f04698df5ca5a5e69151bb97)) +* fix monitor utils incorrect assignment method ([bf280c6](https://github.com/alibaba/lowcode-engine/commit/bf280c6fa1e46d084fc8f20323164816fad4076f)) +* fix outline-pane invisible occasionally when dragging tree node ([031c7f2](https://github.com/alibaba/lowcode-engine/commit/031c7f25f10a6cfebfc7929c9226f4e4167a359f)) +* fix outline-tree initialization failed ([a2d5c6f](https://github.com/alibaba/lowcode-engine/commit/a2d5c6fd90ca0226bbbfea01a4b28c8b8d307a78)) +* fix render module state expression initialization exception ([5bd68ee](https://github.com/alibaba/lowcode-engine/commit/5bd68ee6b448fa58b022870b3f8175d8b77febde)) +* fix render module state expression initialization exception ([9c545cc](https://github.com/alibaba/lowcode-engine/commit/9c545cca6004f65e2f206ea001cefa3fa3cfa807)) +* fix setter hooks error ([8a3a0b8](https://github.com/alibaba/lowcode-engine/commit/8a3a0b824162e25a930711c6fef511b4b369e897)) +* fix test case failures of designer ([4b0521a](https://github.com/alibaba/lowcode-engine/commit/4b0521a47494f78e120f75021e0a076fb00ce53e)) +* Fix the conversion failure of some props expressions under Slot props of low-code components ([7db5461](https://github.com/alibaba/lowcode-engine/commit/7db5461706c739fac673b2466bc2fda7661242e4)) +* fix the leaf hoc component fails to monitor Node changes, and modify the logic for get node ([6ee6b07](https://github.com/alibaba/lowcode-engine/commit/6ee6b07a10ba4aac583def52d8ff1fa78d111d0b)) +* fix the leaf hoc component fails to monitor Node changes, and modify the logic for get node ([f400172](https://github.com/alibaba/lowcode-engine/commit/f4001728259047b09db75d76a8c3ef1e1bcb4e0a)) +* fix the problem that material.getComponentMetasMap returns the wrong result ([e02933c](https://github.com/alibaba/lowcode-engine/commit/e02933c18bc15519b2eba8ad946282502a509611)) +* Fix the rendering error caused by incorrect key value when configuring the loop ([1026763](https://github.com/alibaba/lowcode-engine/commit/1026763dc5a77d4395a1e86e5a0084ab4fb4230c)) +* fix the unit test failure problem caused by thisRequiredInJSE modification ([c2c59b7](https://github.com/alibaba/lowcode-engine/commit/c2c59b7ff72ba06156bbcdb952262739d6188209)) +* fix unnecessary props calculation ([f1fed75](https://github.com/alibaba/lowcode-engine/commit/f1fed75f39be8289ede1ec558b04428a69e25b5f)) +* fixed an issue where materials would be rendered multiple times ([9d187cc](https://github.com/alibaba/lowcode-engine/commit/9d187ccb7de55857e861d3fc881c610506872d03)) +* fixed an issue where materials would be rendered multiple times ([64cc328](https://github.com/alibaba/lowcode-engine/commit/64cc3283c15342151a8f93c46a276681f3575153)) +* fixed focusNodeSelector configuration not taking effect ([9beae9c](https://github.com/alibaba/lowcode-engine/commit/9beae9c3269901bf03a29033121c7d480571bce5)) +* fixed the issue that thisRequiredInJSE did not take effect in some scenarios ([7e5a919](https://github.com/alibaba/lowcode-engine/commit/7e5a919f9352397f11741fd911495996469c0256)) +* in ES require changed to import ([b4d7d6d](https://github.com/alibaba/lowcode-engine/commit/b4d7d6d8c290a335a2c1f60731d4417b23444941)) +* in ES require changed to import ([7c8cd36](https://github.com/alibaba/lowcode-engine/commit/7c8cd36a10a7caa61de31a15abd93ab8a97fbe08)) +* leaf should be type of ShellNode other than InnerNode ([5bb8cf5](https://github.com/alibaba/lowcode-engine/commit/5bb8cf5d12d38d70b69fa28deb2f8aa0afa9b9b9)) +* lowcode component exec lifecycle has error ([f99a47e](https://github.com/alibaba/lowcode-engine/commit/f99a47e502080134454795f5e361cfa4fba3f03b)) +* lowcode component leaf dont have export prop, exec leaf.export make error ([9d51dcd](https://github.com/alibaba/lowcode-engine/commit/9d51dcdae38850be0206861f2cae74ca68805c25)) +* missing engine options config info ([a79875c](https://github.com/alibaba/lowcode-engine/commit/a79875cf8698d3912b50526d97f6ac72e9a21fc9)) +* missing engine options config info ([9ccded0](https://github.com/alibaba/lowcode-engine/commit/9ccded006ef44cd538abaa140250e519243bf090)) +* npm run clean error in windows ([a176e9d](https://github.com/alibaba/lowcode-engine/commit/a176e9d245981fb5718c8d144f477202b3796be6)) +* project event listeners will not be invoked sometimes ([a0c772f](https://github.com/alibaba/lowcode-engine/commit/a0c772fb903cf5eb9e0b811b64bbe3846d4ba8ac)) +* project.exportSchema api lack stage param & setAssets should be a async fn ([0ea76a7](https://github.com/alibaba/lowcode-engine/commit/0ea76a746fac8ea8e7b999d42434c468c85d6372)) +* project.exportSchema should export componentsMap of all documents ([969a130](https://github.com/alibaba/lowcode-engine/commit/969a130b373fb028f8051e96cb9d79f1de0a2a1c)) +* removed incorrectly calling childWhitelist hook logic during drag and drop ([#1141](https://github.com/alibaba/lowcode-engine/issues/1141)) ([6576346](https://github.com/alibaba/lowcode-engine/commit/6576346b9185bedb090be9c84129e077cf5389b3)) +* renderer not rendering correct components when loading components with loadAsyncLibrary api ([9b3b4f9](https://github.com/alibaba/lowcode-engine/commit/9b3b4f9b0e35ef3ea2f0117f0cdb2254e15d5389)) +* should pass index param when creating a Prop instance under a list type Prop instance, fix [#780](https://github.com/alibaba/lowcode-engine/issues/780) ([a8de3f2](https://github.com/alibaba/lowcode-engine/commit/a8de3f299c7b26fa939d2b2ea1428143e2b5fb01)) +* simulator eclipses setting area [#773](https://github.com/alibaba/lowcode-engine/issues/773) ([b4b30a3](https://github.com/alibaba/lowcode-engine/commit/b4b30a359932f5c0e8fde1b28f54a883c87901d8)) +* spec typo ([#1064](https://github.com/alibaba/lowcode-engine/issues/1064)) ([ecb9dca](https://github.com/alibaba/lowcode-engine/commit/ecb9dca2b9386ef6fadfd009d161a9203b9b9558)) +* try catch calculation of dynamic setter ([f61e2a2](https://github.com/alibaba/lowcode-engine/commit/f61e2a2b8a3d8d6754474cd392bc259917c7eb10)) +* type=legao dont make request ([98ececa](https://github.com/alibaba/lowcode-engine/commit/98ececa9c11f93e5f849b201b5b5e7ff453733d7)) +* **types:** rrror declaration of the children prop ([951d1cb](https://github.com/alibaba/lowcode-engine/commit/951d1cb103fa46c0e7926d6138657c7d10cc4f88)) +* use the original object if it is not a shell object ([5ea53f7](https://github.com/alibaba/lowcode-engine/commit/5ea53f706b6571946bcfa56b8655b55717381771)) +* use the outer documentation url of unique key, fixes [#868](https://github.com/alibaba/lowcode-engine/issues/868) ([d770007](https://github.com/alibaba/lowcode-engine/commit/d770007ff8c39e6cf527e07a7d6468dbb88c776d)) +* use the outer documentation url of unique key, fixes [#868](https://github.com/alibaba/lowcode-engine/issues/868) ([912ee22](https://github.com/alibaba/lowcode-engine/commit/912ee22180a424f63298c319c62fb481513af904)) +* use uppercase resize trigger names based on material spec ([7fda0ef](https://github.com/alibaba/lowcode-engine/commit/7fda0efe131e0e2e3141849cf3f87307e7ce1b36)) +* when designMode is not design, the hidden attribute does not take effect ([3dd0b6d](https://github.com/alibaba/lowcode-engine/commit/3dd0b6d0a86267e3029c176ff49aff793ce3e186)) + ### [1.0.4](https://github.com/alibaba/lowcode-engine/compare/@alilc/lowcode-code-generator@1.0.4-beta.0...@alilc/lowcode-code-generator@1.0.4) (2022-04-12) diff --git a/modules/code-generator/CONTRIBUTING.md b/modules/code-generator/CONTRIBUTING.md index c6af74bf71..5f4d373b80 100644 --- a/modules/code-generator/CONTRIBUTING.md +++ b/modules/code-generator/CONTRIBUTING.md @@ -1,10 +1,11 @@ # 如何共建 1. 拉取最新代码,切换到 develop 分支,基于 develop 分支切出一个 feature 或 hotfix 分支 -2. 安装依赖(`npm`),然后先跑一遍 `npm test` 看看是否所有用例都能通过 (如果网络条件不太好,建议使用 [cnpm - 淘宝提供的中国 NPM 镜像](https://npmmirror.com/)) -3. 在 tests 目录下编写您的需求/问题的测试用例 -4. 修改 src 下的一些代码,然后运行 `npm test` 或 `npm start` 启动 jest 进行调测 -5. 确保所有的测试用例都能通过时,提 MR 给 @牧毅 -- MR 将在 1 个工作日内给您回复意见。 +2. 到 `lowcode-engine` 项目根目录下,执行 `lerna bootstrap && lerna run build --scope "@alilc/lowcode-types"` 来安装依赖并构建 +3. 到 `lowcode-engine/modules/code-generator`下,安装依赖(`npm i`),然后先跑一遍 `npm test` 看看是否所有用例都能通过 (如果网络条件不太好,建议使用 [cnpm - 淘宝提供的中国 NPM 镜像](https://npmmirror.com/)) +4. 在 tests 目录下编写您的需求/问题的测试用例 +5. 修改 src 下的一些代码,然后运行 `npm test` 或 `npm start` 启动 jest 进行调测 +6. 确保所有的测试用例都能通过时,提 MR 给 @牧毅 -- MR 将在 1 个工作日内给您回复意见。 当然,欢迎提前私聊沟通 @牧毅,或加入 低代码渲染/出码服务金牌用户群 讨论沟通。 diff --git a/modules/code-generator/README.md b/modules/code-generator/README.md index 03ce94b54e..1d67b3aa16 100644 --- a/modules/code-generator/README.md +++ b/modules/code-generator/README.md @@ -94,16 +94,26 @@ await CodeGenerator.init(); 4. 出码 ```js -const result = await CodeGenerator.generateCode({ +const project = await CodeGenerator.generateCode({ solution: 'icejs', // 出码方案 (目前内置有 icejs 和 rax ) schema, // 编排搭建出来的 schema }); -console.log(result); // 出码结果(默认是递归结构描述的,可以传 flattenResult: true 以生成扁平结构的结果) +console.log(project); // 出码结果(默认是递归结构描述的,可以传 flattenResult: true 以生成扁平结构的结果) ``` 注:一般来说在浏览器中出码适合做即时预览功能。 +5. 下载 zip 包 + +```js +// 写入到 zip 包 +await CodeGenerator.publishers.zip().publish({ + project, // 上一步生成的 project + projectSlug: 'your-project-slug', // 项目标识 -- 对应下载 your-project-slug.zip 文件 +}); +``` + ### 5)自定义出码 前端框架灵活多变,默认内置的出码方案很难满足所有人的需求,好在此代码生成器支持非常灵活的插件机制 -- 欢迎参考 ./src/plugins/xxx 来编写您自己的出码插件,然后参考 ./src/solutions/xxx 将各种插件组合成一套适合您的业务场景的出码方案。 diff --git a/modules/code-generator/babel.config.js b/modules/code-generator/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/modules/code-generator/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/modules/code-generator/bin/lowcode-code-generator.js b/modules/code-generator/bin/lowcode-code-generator.js index 6259a5a6fc..79c36c49a0 100755 --- a/modules/code-generator/bin/lowcode-code-generator.js +++ b/modules/code-generator/bin/lowcode-code-generator.js @@ -14,6 +14,7 @@ program .option('-c, --cwd ', 'specify the working directory', '.') .option('-q, --quiet', 'be quiet, do not output anything unless get error', false) .option('-v, --verbose', 'be verbose, output more information', false) + .option('--solution-options ', 'specify the solution options', '{}') .arguments('[input-schema] ali lowcode schema JSON file') .action(function doGenerate(inputSchema, command) { var options = command.opts(); diff --git a/modules/code-generator/example-schema.json b/modules/code-generator/example-schema.json index aaf3a3e0d8..5e31b1bea9 100644 --- a/modules/code-generator/example-schema.json +++ b/modules/code-generator/example-schema.json @@ -62,17 +62,17 @@ "router": "/" }, "props": { - "ref": "outterView", + "ref": "outerView", "autoLoading": true }, "fileName": "test", "state": { - "text": "outter" + "text": "outer" }, "lifeCycles": { "componentDidMount": { - "type": "JSExpression", - "value": "function() { console.log('componentDidMount'); }" + "type": "JSFunction", + "value": "function componentDidMount() { console.log('componentDidMount'); }" } }, "dataSource": { @@ -91,7 +91,7 @@ "isSync": true }, "dataHandler": { - "type": "JSExpression", + "type": "JSFunction", "value": "function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}" } }, @@ -105,13 +105,13 @@ "isSync": true }, "dataHandler": { - "type": "JSExpression", + "type": "JSFunction", "value": "function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}" } } ], "dataHandler": { - "type": "JSExpression", + "type": "JSFunction", "value": "function (dataMap) {\n console.info(\"All datasources loaded:\", dataMap);\n}" } }, diff --git a/modules/code-generator/example-schema.json5 b/modules/code-generator/example-schema.json5 index f71096fc41..4ab02999ec 100644 --- a/modules/code-generator/example-schema.json5 +++ b/modules/code-generator/example-schema.json5 @@ -62,17 +62,17 @@ router: '/', }, props: { - ref: 'outterView', + ref: 'outerView', autoLoading: true, }, fileName: 'test', state: { - text: 'outter', + text: 'outer', }, lifeCycles: { componentDidMount: { - type: 'JSExpression', - value: "function() { console.log('componentDidMount'); }", + type: 'JSFunction', + value: "function componentDidMount() { console.log('componentDidMount'); }", }, }, dataSource: { @@ -91,7 +91,7 @@ isSync: true, }, dataHandler: { - type: 'JSExpression', + type: 'JSFunction', value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', }, }, @@ -105,13 +105,13 @@ isSync: true, }, dataHandler: { - type: 'JSExpression', + type: 'JSFunction', value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', }, }, ], dataHandler: { - type: 'JSExpression', + type: 'JSFunction', value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', }, }, diff --git a/modules/code-generator/package.json b/modules/code-generator/package.json index 18eb9af638..cd114fa067 100644 --- a/modules/code-generator/package.json +++ b/modules/code-generator/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-code-generator", - "version": "1.0.5", + "version": "1.1.7", "description": "出码引擎 for LowCode Engine", "license": "MIT", "main": "lib/index.js", @@ -47,6 +47,11 @@ "prepublishOnly": "npm run build", "demo": "node bin/lowcode-code-generator.js -i example-schema.json -o demo -s icejs" }, + "standard-version": { + "skip": { + "changelog": true + } + }, "husky": { "hooks": { "pre-commit": "lint-staged", @@ -75,6 +80,7 @@ "change-case": "^3.1.0", "commander": "^6.1.0", "debug": "^4.3.2", + "file-saver": "^2.0.5", "fp-ts": "^2.11.9", "fs-extra": "9.x", "glob": "^7.2.0", @@ -93,6 +99,7 @@ "qs": "^6.10.1", "semver": "^7.3.4", "short-uuid": "^3.1.1", + "babel-jest": "^26.5.2", "tslib": "^2.3.1" }, "browser": { @@ -103,6 +110,7 @@ "devDependencies": { "@iceworks/spec": "^1.4.2", "@types/babel__traverse": "^7.11.0", + "@types/file-saver": "^2.0.7", "@types/jest": "^27.0.2", "@types/lodash": "^4.14.162", "@types/node": "^14.14.20", @@ -120,11 +128,11 @@ "eslint-plugin-import": "^2.22.1", "eslint-plugin-react": "^7.22.0", "eslint-plugin-react-hooks": "^4.2.0", - "jest": "^27.4.7", + "jest": "^26.5.2", "jest-util": "^27.4.2", "rimraf": "^3.0.2", "standard-version": "^9.1.1", - "ts-jest": "^27.1.3", + "ts-jest": "^26.5.2", "ts-loader": "^6.2.2", "ts-node": "^8.10.2", "tsconfig-paths": "^3.9.0", @@ -137,5 +145,11 @@ "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org/" - } + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main/modules/code-generator" + }, + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/modules/code-generator/src/analyzer/componentAnalyzer.ts b/modules/code-generator/src/analyzer/componentAnalyzer.ts index 952295a15f..69e8ad482d 100644 --- a/modules/code-generator/src/analyzer/componentAnalyzer.ts +++ b/modules/code-generator/src/analyzer/componentAnalyzer.ts @@ -1,13 +1,13 @@ -import type { NodeSchema, CompositeObject } from '@alilc/lowcode-types'; +import type { IPublicTypeNodeSchema, IPublicTypeCompositeObject } from '@alilc/lowcode-types'; import type { TComponentAnalyzer } from '../types'; import { handleSubNodes } from '../utils/schema'; export const componentAnalyzer: TComponentAnalyzer = (container) => { let hasRefAttr = false; - const nodeValidator = (n: NodeSchema) => { + const nodeValidator = (n: IPublicTypeNodeSchema) => { if (n.props) { - const props = n.props as CompositeObject; + const props = n.props as IPublicTypeCompositeObject; if (props.ref) { hasRefAttr = true; } diff --git a/modules/code-generator/src/cli/run.ts b/modules/code-generator/src/cli/run.ts index db66b46013..ec6814f767 100644 --- a/modules/code-generator/src/cli/run.ts +++ b/modules/code-generator/src/cli/run.ts @@ -9,7 +9,7 @@ import * as path from 'path'; import { getErrorMessage } from '../utils/errors'; import CodeGenerator from '..'; import type { IProjectBuilder } from '..'; -import type { ProjectSchema } from '@alilc/lowcode-types'; +import type { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; /** * 执行出码 CLI 命令 @@ -25,6 +25,7 @@ export async function run( output?: string; quiet?: boolean; verbose?: boolean; + solutionOptions?: string; }, ): Promise { try { @@ -41,6 +42,19 @@ export async function run( ); } + let solutionOptions = {}; + + if (options.solutionOptions) { + try { + solutionOptions = JSON.parse(options.solutionOptions); + } catch (err: any) { + throw new Error( + `solution options parse error, error message is "${err.message}"`, + ); + } + } + + // 读取 Schema const schema = await loadSchemaFile(schemaFile); @@ -48,7 +62,7 @@ export async function run( const createProjectBuilder = await getProjectBuilderFactory(options.solution, { quiet: options.quiet, }); - const builder = createProjectBuilder(); + const builder = createProjectBuilder(solutionOptions); // 生成代码 const generatedSourceCodes = await builder.generateProject(schema); @@ -75,7 +89,7 @@ export async function run( async function getProjectBuilderFactory( solution: string, { quiet }: { quiet?: boolean }, -): Promise<() => IProjectBuilder> { +): Promise<(options: {[prop: string]: any}) => IProjectBuilder> { if (solution in CodeGenerator.solutions) { return CodeGenerator.solutions[solution as 'icejs' | 'rax']; } @@ -117,7 +131,7 @@ function isLocalSolution(solution: string) { return solution.startsWith('.') || solution.startsWith('/') || solution.startsWith('~'); } -async function loadSchemaFile(schemaFile: string): Promise { +async function loadSchemaFile(schemaFile: string): Promise { if (!schemaFile) { throw new Error('invalid schema file name'); } diff --git a/modules/code-generator/src/cli/solutions/example-solution.ts b/modules/code-generator/src/cli/solutions/example-solution.ts index 2efff780cd..bfb9d079b1 100644 --- a/modules/code-generator/src/cli/solutions/example-solution.ts +++ b/modules/code-generator/src/cli/solutions/example-solution.ts @@ -289,8 +289,8 @@ codealike.json }, "lifeCycles": { "componentDidMount": { - "type": "JSExpression", - "value": "function() { console.log('componentDidMount'); }" + "type": "JSFunction", + "value": "function componentDidMount() {\\n console.log('componentDidMount');\\n}" } }, "methodsModule": { @@ -559,8 +559,8 @@ codealike.json "registry": "https://registry.npm.xxx.com" }, "dependencies": { - "@alilc/lowcode-code-generator": "^1.0.0-beta.16", - "@alilc/lowcode-types": "^1.0.0-beta.21", + "@alilc/lowcode-code-generator": "^1.0.0", + "@alilc/lowcode-types": "^1.0.0", "tslib": "^2.3.0" }, "devDependencies": { @@ -635,18 +635,20 @@ export default function createHelloWorldProjectBuilder() { template: CodeGen.solutionParts.icejs.template, plugins: { components: [ - CodeGen.plugins.react.reactCommonDeps(), - CodeGen.plugins.common.esmodule({ fileType: 'jsx' }), - CodeGen.plugins.react.containerClass(), - CodeGen.plugins.react.containerInjectContext(), - CodeGen.plugins.react.containerInjectUtils(), - CodeGen.plugins.react.containerInjectDataSourceEngine(), - CodeGen.plugins.react.containerInjectI18n(), - CodeGen.plugins.react.containerInitState(), - CodeGen.plugins.react.containerLifeCycle(), - CodeGen.plugins.react.containerMethod(), + CodeGen.plugins.icejs.reactCommonDeps(), + CodeGen.plugins.common.esModule({ fileType: 'jsx' }), + CodeGen.plugins.common.styleImport(), + CodeGen.plugins.icejs.containerClass(), + CodeGen.plugins.icejs.containerInjectContext(), + CodeGen.plugins.icejs.containerInjectUtils(), + CodeGen.plugins.icejs.containerInjectDataSourceEngine(), + CodeGen.plugins.icejs.containerInjectI18n(), + CodeGen.plugins.icejs.containerInjectConstants(), + CodeGen.plugins.icejs.containerInitState(), + CodeGen.plugins.icejs.containerLifeCycle(), + CodeGen.plugins.icejs.containerMethod(), examplePlugin(), - CodeGen.plugins.react.jsx({ + CodeGen.plugins.icejs.jsx({ nodeTypeMapping: { Div: 'div', Component: 'div', @@ -657,18 +659,20 @@ export default function createHelloWorldProjectBuilder() { CodeGen.plugins.style.css(), ], pages: [ - CodeGen.plugins.react.reactCommonDeps(), - CodeGen.plugins.common.esmodule({ fileType: 'jsx' }), - CodeGen.plugins.react.containerClass(), - CodeGen.plugins.react.containerInjectContext(), - CodeGen.plugins.react.containerInjectUtils(), - CodeGen.plugins.react.containerInjectDataSourceEngine(), - CodeGen.plugins.react.containerInjectI18n(), - CodeGen.plugins.react.containerInitState(), - CodeGen.plugins.react.containerLifeCycle(), - CodeGen.plugins.react.containerMethod(), + CodeGen.plugins.icejs.reactCommonDeps(), + CodeGen.plugins.common.esModule({ fileType: 'jsx' }), + CodeGen.plugins.common.styleImport(), + CodeGen.plugins.icejs.containerClass(), + CodeGen.plugins.icejs.containerInjectContext(), + CodeGen.plugins.icejs.containerInjectUtils(), + CodeGen.plugins.icejs.containerInjectDataSourceEngine(), + CodeGen.plugins.icejs.containerInjectI18n(), + CodeGen.plugins.icejs.containerInjectConstants(), + CodeGen.plugins.icejs.containerInitState(), + CodeGen.plugins.icejs.containerLifeCycle(), + CodeGen.plugins.icejs.containerMethod(), examplePlugin(), - CodeGen.plugins.react.jsx({ + CodeGen.plugins.icejs.jsx({ nodeTypeMapping: { Div: 'div', Component: 'div', @@ -679,13 +683,13 @@ export default function createHelloWorldProjectBuilder() { CodeGen.plugins.style.css(), ], router: [ - CodeGen.plugins.common.esmodule(), + CodeGen.plugins.common.esModule(), CodeGen.solutionParts.icejs.plugins.router(), ], entry: [CodeGen.solutionParts.icejs.plugins.entry()], constants: [CodeGen.plugins.project.constants()], utils: [ - CodeGen.plugins.common.esmodule(), + CodeGen.plugins.common.esModule(), CodeGen.plugins.project.utils('react'), ], i18n: [CodeGen.plugins.project.i18n()], diff --git a/modules/code-generator/src/const/index.ts b/modules/code-generator/src/const/index.ts index 103d912db6..24449ca429 100644 --- a/modules/code-generator/src/const/index.ts +++ b/modules/code-generator/src/const/index.ts @@ -8,5 +8,26 @@ export const CONTAINER_TYPE = { export const SUPPORT_SCHEMA_VERSION_LIST = ['0.0.1', '1.0.0']; +// built-in slot names which have been handled in ProjectBuilder +export const BUILTIN_SLOT_NAMES = [ + 'pages', + 'components', + 'router', + 'entry', + 'appConfig', + 'buildConfig', + 'constants', + 'utils', + 'i18n', + 'globalStyle', + 'htmlEntry', + 'packageJSON', + 'demo', +]; + +export const isBuiltinSlotName = function (name: string) { + return BUILTIN_SLOT_NAMES.includes(name); +}; + export * from './file'; export * from './generator'; diff --git a/modules/code-generator/src/generator/ModuleBuilder.ts b/modules/code-generator/src/generator/ModuleBuilder.ts index f4554ef612..e172f716e5 100644 --- a/modules/code-generator/src/generator/ModuleBuilder.ts +++ b/modules/code-generator/src/generator/ModuleBuilder.ts @@ -1,4 +1,4 @@ -import { ProjectSchema, ResultFile, ResultDir } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema, ResultFile, ResultDir } from '@alilc/lowcode-types'; import { BuilderComponentPlugin, @@ -62,10 +62,9 @@ export function createModuleBuilder( if (options.postProcessors.length > 0) { files = files.map((file) => { - let { content } = file; - const type = file.ext; + let { content, ext: type, name } = file; options.postProcessors.forEach((processer) => { - content = processer(content, type); + content = processer(content, type, name); }); return createResultFile(file.name, type, content); @@ -77,7 +76,7 @@ export function createModuleBuilder( }; }; - const generateModuleCode = async (schema: ProjectSchema | string): Promise => { + const generateModuleCode = async (schema: IPublicTypeProjectSchema | string): Promise => { // Init const schemaParser: ISchemaParser = new SchemaParser(); const parseResult: IParseResult = schemaParser.parse(schema); diff --git a/modules/code-generator/src/generator/ProjectBuilder.ts b/modules/code-generator/src/generator/ProjectBuilder.ts index 2a1282d6eb..a910b49002 100644 --- a/modules/code-generator/src/generator/ProjectBuilder.ts +++ b/modules/code-generator/src/generator/ProjectBuilder.ts @@ -1,4 +1,4 @@ -import { ResultDir, ResultFile, ProjectSchema } from '@alilc/lowcode-types'; +import { ResultDir, ResultFile, IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { IModuleBuilder, @@ -16,6 +16,7 @@ import { createResultDir, addDirectory, addFile } from '../utils/resultHelper'; import { createModuleBuilder } from './ModuleBuilder'; import { ProjectPreProcessor, ProjectPostProcessor, IContextData } from '../types/core'; import { CodeGeneratorError } from '../types/error'; +import { isBuiltinSlotName } from '../const'; interface IModuleInfo { moduleName?: string; @@ -24,22 +25,36 @@ interface IModuleInfo { } export interface ProjectBuilderInitOptions { + /** 项目模板 */ template: IProjectTemplate; + /** 项目插件 */ plugins: IProjectPlugins; + /** 模块后置处理器 */ postProcessors: PostProcessor[]; + /** Schema 解析器 */ schemaParser?: ISchemaParser; + /** 项目级别的前置处理器 */ projectPreProcessors?: ProjectPreProcessor[]; + /** 项目级别的后置处理器 */ projectPostProcessors?: ProjectPostProcessor[]; + /** 是否处于严格模式 */ inStrictMode?: boolean; + /** 一些额外的上下文数据 */ extraContextData?: Record; + + /** + * Hook which is used to customize original options, we can reorder/add/remove plugins/processors + * of the existing solution. + */ + customizeBuilderOptions?(originalOptions: ProjectBuilderInitOptions): ProjectBuilderInitOptions; } export class ProjectBuilder implements IProjectBuilder { @@ -62,21 +77,26 @@ export class ProjectBuilder implements IProjectBuilder { private projectPostProcessors: ProjectPostProcessor[]; /** 是否处于严格模式 */ - public readonly inStrictMode: boolean; + readonly inStrictMode: boolean; /** 一些额外的上下文数据 */ - public readonly extraContextData: IContextData; - - constructor({ - template, - plugins, - postProcessors, - schemaParser = new SchemaParser(), - projectPreProcessors = [], - projectPostProcessors = [], - inStrictMode = false, - extraContextData = {}, - }: ProjectBuilderInitOptions) { + readonly extraContextData: IContextData; + + constructor(builderOptions: ProjectBuilderInitOptions) { + let customBuilderOptions = builderOptions; + if (typeof builderOptions.customizeBuilderOptions === 'function') { + customBuilderOptions = builderOptions.customizeBuilderOptions(builderOptions); + } + const { + template, + plugins, + postProcessors, + schemaParser = new SchemaParser(), + projectPreProcessors = [], + projectPostProcessors = [], + inStrictMode = false, + extraContextData = {}, + } = customBuilderOptions; this.template = template; this.plugins = plugins; this.postProcessors = postProcessors; @@ -87,34 +107,39 @@ export class ProjectBuilder implements IProjectBuilder { this.extraContextData = extraContextData; } - async generateProject(originalSchema: ProjectSchema | string): Promise { + async generateProject(originalSchema: IPublicTypeProjectSchema | string): Promise { // Init const { schemaParser } = this; - const builders = this.createModuleBuilders(); - - const projectRoot = await this.template.generateTemplate(); - let schema: ProjectSchema = + let schema: IPublicTypeProjectSchema = typeof originalSchema === 'string' ? JSON.parse(originalSchema) : originalSchema; - // Validate - if (!schemaParser.validate(schema)) { - throw new CodeGeneratorError('Schema is invalid'); - } - // Parse / Format - // Preprocess for (const preProcessor of this.projectPreProcessors) { // eslint-disable-next-line no-await-in-loop schema = await preProcessor(schema); } + // Validate + if (!schemaParser.validate(schema)) { + throw new CodeGeneratorError('Schema is invalid'); + } + // Collect Deps // Parse JSExpression const parseResult: IParseResult = schemaParser.parse(schema); + + const projectRoot = await this.template.generateTemplate(parseResult); + let buildResult: IModuleInfo[] = []; + const builders = this.createModuleBuilders({ + extraContextData: { + projectRemark: parseResult?.project?.projectRemark, + template: this.template, + }, + }); // Generator Code module // components // pages @@ -241,14 +266,25 @@ export class ProjectBuilder implements IProjectBuilder { }); } - // TODO: 更多 slots 的处理??是不是可以考虑把 template 中所有的 slots 都处理下? + // demo + if (parseResult.project && builders.demo) { + const { files } = await builders.demo.generateModule(parseResult.project); + buildResult.push({ + path: this.template.slots.demo.path, + files, + }); + } - // Post Process + // handle extra slots + await this.generateExtraSlots(builders, parseResult, buildResult); + // Post Process + const isSingleComponent = parseResult?.project?.projectRemark?.isSingleComponent; // Combine Modules buildResult.forEach((moduleInfo) => { let targetDir = getDirFromRoot(projectRoot, moduleInfo.path); - if (moduleInfo.moduleName) { + // if project only contain single component, skip creation of directory. + if (moduleInfo.moduleName && !isSingleComponent) { const dir = createResultDir(moduleInfo.moduleName); addDirectory(targetDir, dir); targetDir = dir; @@ -260,13 +296,17 @@ export class ProjectBuilder implements IProjectBuilder { let finalResult = projectRoot; for (const projectPostProcessor of this.projectPostProcessors) { // eslint-disable-next-line no-await-in-loop - finalResult = await projectPostProcessor(finalResult, schema, originalSchema); + finalResult = await projectPostProcessor(finalResult, schema, originalSchema, { + template: this.template, + parseResult, + }); } return finalResult; } - private createModuleBuilders(): Record { + private createModuleBuilders(extraContextData: Record = {}): + Record { const builders: Record = {}; Object.keys(this.plugins).forEach((pluginName) => { @@ -279,10 +319,12 @@ export class ProjectBuilder implements IProjectBuilder { plugins: this.plugins[pluginName], postProcessors: this.postProcessors, contextData: { + // template: this.template, inStrictMode: this.inStrictMode, tolerateEvalErrors: true, evalErrorsHandler: '', ...this.extraContextData, + ...extraContextData, }, ...options, }); @@ -291,6 +333,22 @@ export class ProjectBuilder implements IProjectBuilder { return builders; } + + private async generateExtraSlots( + builders: Record, + parseResult: IParseResult, + buildResult: IModuleInfo[], + ) { + for (const slotName in this.template.slots) { + if (!isBuiltinSlotName(slotName)) { + const { files } = await builders[slotName].generateModule(parseResult); + buildResult.push({ + path: this.template.slots[slotName].path, + files, + }); + } + } + } } export function createProjectBuilder(initOptions: ProjectBuilderInitOptions): IProjectBuilder { diff --git a/modules/code-generator/src/index.ts b/modules/code-generator/src/index.ts index 7cf861e6f8..da9b12d877 100644 --- a/modules/code-generator/src/index.ts +++ b/modules/code-generator/src/index.ts @@ -1,6 +1,6 @@ /** * 低代码引擎的出码模块,负责将编排产出的 Schema 转换成实际可执行的代码。 - * 注意:为了保持 API 的稳定性, 这里所有导出的 API 均要显式命名方式导出 + * 注意:为了保持 API 的稳定性,这里所有导出的 API 均要显式命名方式导出 * (即用 export { xxx } from 'xx' 的方式,不要直接 export * from 'xxx') * 而且所有导出的 API 务必在 tests/public 中编写单元测试 */ @@ -8,7 +8,8 @@ import { createProjectBuilder } from './generator/ProjectBuilder'; import { createModuleBuilder } from './generator/ModuleBuilder'; import { createDiskPublisher } from './publisher/disk'; import { createZipPublisher } from './publisher/zip'; -import createIceJsProjectBuilder, { plugins as reactPlugins } from './solutions/icejs'; +import createIceJsProjectBuilder, { plugins as icejsPlugins } from './solutions/icejs'; +import createIceJs3ProjectBuilder, { plugins as icejs3Plugins } from './solutions/icejs3'; import createRaxAppProjectBuilder, { plugins as raxPlugins } from './solutions/rax-app'; // 引入说明 @@ -18,6 +19,7 @@ import { COMMON_CHUNK_NAME, CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from ' // 引入通用插件组 import esmodule from './plugins/common/esmodule'; import requireUtils from './plugins/common/requireUtils'; +import styleImport from './plugins/common/styleImport'; import css from './plugins/component/style/css'; import constants from './plugins/project/constants'; @@ -32,6 +34,7 @@ import * as CONSTANTS from './const'; // 引入内置解决方案模块 import icejs from './plugins/project/framework/icejs'; +import icejs3 from './plugins/project/framework/icejs3'; import rax from './plugins/project/framework/rax'; export default { @@ -39,10 +42,12 @@ export default { createModuleBuilder, solutions: { icejs: createIceJsProjectBuilder, + icejs3: createIceJs3ProjectBuilder, rax: createRaxAppProjectBuilder, }, solutionParts: { icejs, + icejs3, rax, }, publishers: { @@ -51,6 +56,7 @@ export default { }, plugins: { common: { + /** * 处理 ES Module * @deprecated please use esModule @@ -58,12 +64,7 @@ export default { esmodule, esModule: esmodule, requireUtils, - }, - react: { - ...reactPlugins, - }, - rax: { - ...raxPlugins, + styleImport, }, style: { css, @@ -73,6 +74,22 @@ export default { i18n, utils, }, + icejs: { + ...icejsPlugins, + }, + icejs3: { + ...icejs3Plugins, + }, + rax: { + ...raxPlugins, + }, + + /** + * @deprecated please use icejs + */ + react: { + ...icejsPlugins, + }, }, postprocessor: { prettier, diff --git a/modules/code-generator/src/parser/SchemaParser.ts b/modules/code-generator/src/parser/SchemaParser.ts index 2e526d88bc..b4f7424ce5 100644 --- a/modules/code-generator/src/parser/SchemaParser.ts +++ b/modules/code-generator/src/parser/SchemaParser.ts @@ -4,14 +4,14 @@ */ import changeCase from 'change-case'; import { - UtilItem, - NodeDataType, - NodeSchema, - ContainerSchema, - ProjectSchema, - PropsMap, - NodeData, - NpmInfo, + IPublicTypeUtilItem, + IPublicTypeNodeDataType, + IPublicTypeNodeSchema, + IPublicTypeContainerSchema, + IPublicTypeProjectSchema, + IPublicTypePropsMap, + IPublicTypeNodeData, + IPublicTypeNpmInfo, } from '@alilc/lowcode-types'; import { IPageMeta, @@ -32,10 +32,11 @@ import { import { SUPPORT_SCHEMA_VERSION_LIST } from '../const'; import { getErrorMessage } from '../utils/errors'; -import { handleSubNodes } from '../utils/schema'; +import { handleSubNodes, isValidContainerType, ContainerType } from '../utils/schema'; import { uniqueArray } from '../utils/common'; import { componentAnalyzer } from '../analyzer/componentAnalyzer'; import { ensureValidClassName } from '../utils/validate'; +import type { ProjectRemark } from '../types/intermediate'; const defaultContainer: IContainerInfo = { containerType: 'Component', @@ -71,18 +72,18 @@ function getRootComponentName(typeName: string, maps: Record, depName: string) { + const dep = internalDeps[depName]; + return (dep && dep.type !== InternalDependencyType.PAGE) ? dep : null; +} + export class SchemaParser implements ISchemaParser { - validate(schema: ProjectSchema): boolean { + validate(schema: IPublicTypeProjectSchema): boolean { if (SUPPORT_SCHEMA_VERSION_LIST.indexOf(schema.version) < 0) { throw new CompatibilityError(`Not support schema with version [${schema.version}]`); } @@ -114,7 +120,7 @@ export class SchemaParser implements ISchemaParser { return true; } - parse(schemaSrc: ProjectSchema | string): IParseResult { + parse(schemaSrc: IPublicTypeProjectSchema | string): IParseResult { // TODO: collect utils depends in JSExpression const compDeps: Record = {}; const internalDeps: Record = {}; @@ -139,9 +145,9 @@ export class SchemaParser implements ISchemaParser { let containers: IContainerInfo[]; // Test if this is a lowcode component without container if (schema.componentsTree.length > 0) { - const firstRoot: ContainerSchema = schema.componentsTree[0] as ContainerSchema; + const firstRoot: IPublicTypeContainerSchema = schema.componentsTree[0] as IPublicTypeContainerSchema; - if (!('fileName' in firstRoot) || !firstRoot.fileName) { + if (!firstRoot.fileName && !isValidContainerType(firstRoot)) { // 整个 schema 描述一个容器,且无根节点定义 const container: IContainerInfo = { ...firstRoot, @@ -149,18 +155,19 @@ export class SchemaParser implements ISchemaParser { props: firstRoot.props || defaultContainer.props, css: firstRoot.css || defaultContainer.css, moduleName: (firstRoot as IContainerInfo).moduleName || defaultContainer.moduleName, - children: schema.componentsTree as NodeSchema[], + children: schema.componentsTree as IPublicTypeNodeSchema[], }; containers = [container]; } else { // 普通带 1 到多个容器的 schema containers = schema.componentsTree.map((n) => { - const subRoot = n as ContainerSchema; + const subRoot = n as IPublicTypeContainerSchema; const container: IContainerInfo = { ...subRoot, componentName: getRootComponentName(subRoot.componentName, compDeps), containerType: subRoot.componentName, - moduleName: ensureValidClassName(changeCase.pascalCase(subRoot.fileName)), + moduleName: ensureValidClassName(subRoot.componentName === ContainerType.Component ? + subRoot.fileName : changeCase.pascalCase(subRoot.fileName)), }; return container; }); @@ -172,7 +179,7 @@ export class SchemaParser implements ISchemaParser { // 分析引用能力的依赖 containers = containers.map((con) => ({ ...con, - analyzeResult: componentAnalyzer(con as ContainerSchema), + analyzeResult: componentAnalyzer(con as IPublicTypeContainerSchema), })); // 建立所有容器的内部依赖索引 @@ -210,7 +217,7 @@ export class SchemaParser implements ISchemaParser { handleSubNodes( container.children, { - node: (i: NodeSchema) => processChildren(i), + node: (i: IPublicTypeNodeSchema) => processChildren(i), }, { rerun: true, @@ -219,12 +226,11 @@ export class SchemaParser implements ISchemaParser { } }); - // 分析容器内部组件依赖 containers.forEach((container) => { const depNames = this.getComponentNames(container); // eslint-disable-next-line no-param-reassign container.deps = uniqueArray(depNames, (i: string) => i) - .map((depName) => internalDeps[depName] || compDeps[depName]) + .map((depName) => getInternalDep(internalDeps, depName) || compDeps[depName]) .filter(Boolean); // container.deps = Object.keys(compDeps).map((depName) => compDeps[depName]); }); @@ -254,13 +260,12 @@ export class SchemaParser implements ISchemaParser { .filter((dep) => !!dep); // 分析 Utils 依赖 - let utils: UtilItem[]; + let utils: IPublicTypeUtilItem[]; if (schema.utils) { utils = schema.utils; utilsDeps = schema.utils .filter( - (u): u is { name: string; type: 'npm' | 'tnpm'; content: NpmInfo } => - u.type !== 'function', + (u): u is { name: string; type: 'npm' | 'tnpm'; content: IPublicTypeNpmInfo } => u.type !== 'function', ) .map( (u): IExternalDependency => ({ @@ -320,15 +325,22 @@ export class SchemaParser implements ISchemaParser { utilsDeps, packages: npms || [], dataSourcesTypes: this.collectDataSourcesTypes(schema), + projectRemark: this.getProjectRemark(containers), }, }; } - getComponentNames(children: NodeDataType): string[] { + getProjectRemark(containers: IContainerInfo[]): ProjectRemark { + return { + isSingleComponent: containers.length === 1 && containers[0].containerType === 'Component', + }; + } + + getComponentNames(children: IPublicTypeNodeDataType): string[] { return handleSubNodes( children, { - node: (i: NodeSchema) => i.componentName, + node: (i: IPublicTypeNodeSchema) => i.componentName, }, { rerun: true, @@ -336,8 +348,8 @@ export class SchemaParser implements ISchemaParser { ); } - decodeSchema(schemaSrc: string | ProjectSchema): ProjectSchema { - let schema: ProjectSchema; + decodeSchema(schemaSrc: string | IPublicTypeProjectSchema): IPublicTypeProjectSchema { + let schema: IPublicTypeProjectSchema; if (typeof schemaSrc === 'string') { try { schema = JSON.parse(schemaSrc); @@ -352,7 +364,7 @@ export class SchemaParser implements ISchemaParser { return schema; } - private collectDataSourcesTypes(schema: ProjectSchema): string[] { + private collectDataSourcesTypes(schema: IPublicTypeProjectSchema): string[] { const dataSourcesTypes = new Set(); // 数据源的默认类型为 fetch diff --git a/modules/code-generator/src/plugins/common/esmodule.ts b/modules/code-generator/src/plugins/common/esmodule.ts index ee28083d15..53f3f9418b 100644 --- a/modules/code-generator/src/plugins/common/esmodule.ts +++ b/modules/code-generator/src/plugins/common/esmodule.ts @@ -443,6 +443,7 @@ function buildPackageImport( export interface PluginConfig { fileType?: string; // 导出的文件类型 useAliasName?: boolean; // 是否使用 componentName 重命名组件 identifier + filter?: (deps: IDependency[]) => IDependency[]; // 支持过滤能力 } const pluginFactory: BuilderComponentPluginFactory = (config?: PluginConfig) => { @@ -460,7 +461,8 @@ const pluginFactory: BuilderComponentPluginFactory = (config?: Plu const ir = next.ir as IWithDependency; if (ir && ir.deps && ir.deps.length > 0) { - const packs = groupDepsByPack(ir.deps); + const deps = cfg.filter ? cfg.filter(ir.deps) : ir.deps; + const packs = groupDepsByPack(deps); Object.keys(packs).forEach((pkg) => { const chunks = buildPackageImport(pkg, packs[pkg], cfg.fileType, cfg.useAliasName); diff --git a/modules/code-generator/src/plugins/common/styleImport.ts b/modules/code-generator/src/plugins/common/styleImport.ts new file mode 100644 index 0000000000..23e1293025 --- /dev/null +++ b/modules/code-generator/src/plugins/common/styleImport.ts @@ -0,0 +1,64 @@ +import changeCase from 'change-case'; +import { + FileType, + BuilderComponentPluginFactory, + BuilderComponentPlugin, + ICodeStruct, + IWithDependency, + ChunkType, +} from '../../types'; + +import { COMMON_CHUNK_NAME } from '../../const/generator'; + +const pluginFactory: BuilderComponentPluginFactory = () => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IWithDependency; + const { chunks } = next; + + if (ir && ir.deps && ir.deps.length > 0) { + let lowcodeMaterialsStyleAdded = false; + let fusionUIStyleAdded = false; + let nextStyleAddedMap: Record = {}; + ir.deps.forEach((dep: any) => { + if (dep.package === '@alifd/next' && !nextStyleAddedMap[dep.exportName]) { + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: `import '@alifd/next/lib/${changeCase.paramCase(dep.exportName)}/style';`, + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + nextStyleAddedMap[dep.exportName] = true; + } else if (dep.package === '@alilc/lowcode-materials' && !lowcodeMaterialsStyleAdded) { + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: 'import \'@alilc/lowcode-materials/lib/style\';', + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + lowcodeMaterialsStyleAdded = true; + } else if (dep.package === '@alifd/fusion-ui' && !fusionUIStyleAdded) { + chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: 'import \'@alifd/fusion-ui/lib/style\';', + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + fusionUIStyleAdded = true; + } + }); + } + + return next; + }; + + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts b/modules/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts index 446b1d3f89..cb7a9e8c36 100644 --- a/modules/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts +++ b/modules/code-generator/src/plugins/component/rax/containerInjectDataSourceEngine.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/indent */ import { - CompositeValue, - JSExpression, + IPublicTypeCompositeValue, + IPublicTypeJSExpression, InterpretDataSourceConfig, isJSExpression, isJSFunction, @@ -56,7 +56,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => (dataSourceConfig && dataSourceConfig.list) || []; const dataSourceEngineOptions = { runtimeConfig: true }; if (dataSourceItems.length > 0) { - const requestHandlersMap: Record = {}; + const requestHandlersMap: Record = {}; dataSourceItems.forEach((ds) => { const dsType = ds.type || 'fetch'; @@ -178,7 +178,7 @@ _defineDataSourceConfig() { export default pluginFactory; -function wrapAsFunction(value: CompositeValue, scope: IScope): CompositeValue { +function wrapAsFunction(value: IPublicTypeCompositeValue, scope: IScope): IPublicTypeCompositeValue { if (isJSExpression(value) || isJSFunction(value)) { return { type: 'JSExpression', diff --git a/modules/code-generator/src/plugins/component/rax/containerLifeCycle.ts b/modules/code-generator/src/plugins/component/rax/containerLifeCycle.ts index f928c3995e..bbee6367f2 100644 --- a/modules/code-generator/src/plugins/component/rax/containerLifeCycle.ts +++ b/modules/code-generator/src/plugins/component/rax/containerLifeCycle.ts @@ -13,6 +13,7 @@ import { IContainerInfo, } from '../../../types'; import { debug } from '../../../utils/debug'; +import { isJSExpressionFn } from '../../../utils/common'; export interface PluginConfig { fileType: string; @@ -49,6 +50,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 过滤掉非法数据(有些场景下会误传入空字符串或 null) if ( !isJSFunction(lifeCycles[lifeCycleName]) && + !isJSExpressionFn(lifeCycles[lifeCycleName]) && !isJSExpression(lifeCycles[lifeCycleName]) ) { return; diff --git a/modules/code-generator/src/plugins/component/rax/jsx.ts b/modules/code-generator/src/plugins/component/rax/jsx.ts index ebc72104f0..ddae619cd6 100644 --- a/modules/code-generator/src/plugins/component/rax/jsx.ts +++ b/modules/code-generator/src/plugins/component/rax/jsx.ts @@ -1,8 +1,8 @@ import { - NodeSchema, - JSExpression, - NpmInfo, - CompositeValue, + IPublicTypeNodeSchema, + IPublicTypeJSExpression, + IPublicTypeNpmInfo, + IPublicTypeCompositeValue, isJSExpression, } from '@alilc/lowcode-types'; @@ -75,8 +75,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 注意:这里其实隐含了一个假设:schema 中的 componentName 应该是一个有效的 JS 标识符,而且是大写字母打头的 // FIXME: 为了快速修复临时加的逻辑,需要用 pre-process 的方式替代处理。 - const mapComponentNameToAliasOrKeepIt = (componentName: string) => - componentsNameAliasMap.get(componentName) || componentName; + const mapComponentNameToAliasOrKeepIt = (componentName: string) => componentsNameAliasMap.get(componentName) || componentName; // 然后过滤掉所有的别名 chunks next.chunks = next.chunks.filter((chunk) => !isImportAliasDefineChunk(chunk)); @@ -86,7 +85,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 2. 小程序出码的时候,很容易出现 Uncaught TypeError: Cannot read property 'avatar' of undefined 这样的异常(如下图的 50 行) -- 因为若直接出码,Rax 构建到小程序的时候会立即计算所有在视图中用到的变量 // 3. 通过 this.xxx 能拿到的东西太多了,而且自定义的 methods 可能会无意间破坏 Rax 框架或小程序框架在页面 this 上的东东 const customHandlers: HandlerSet = { - expression(input: JSExpression, scope: IScope) { + expression(input: IPublicTypeJSExpression, scope: IScope) { return transformJsExpr(generateExpression(input, scope), scope, { dontWrapEval: !tolerateEvalErrors, }); @@ -147,7 +146,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { ${evalErrorsHandler} } } @@ -171,7 +170,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => return next; function generateRaxLoopCtrl( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, next?: NodePlugin, @@ -218,7 +217,7 @@ function isImportAliasDefineChunk(chunk: ICodeChunk): chunk is ICodeChunk & { ext: { aliasName: string; originalName: string; - dependency: NpmInfo; + dependency: IPublicTypeNpmInfo; }; } { return ( @@ -226,13 +225,13 @@ function isImportAliasDefineChunk(chunk: ICodeChunk): chunk is ICodeChunk & { !!chunk.ext && typeof chunk.ext.aliasName === 'string' && typeof chunk.ext.originalName === 'string' && - !!(chunk.ext.dependency as NpmInfo | null)?.componentName + !!(chunk.ext.dependency as IPublicTypeNpmInfo | null)?.componentName ); } function generateNodeAttrForRax( this: { cfg: PluginConfig }, - attrData: { attrName: string; attrValue: CompositeValue }, + attrData: { attrName: string; attrValue: IPublicTypeCompositeValue }, scope: IScope, config?: NodeGeneratorConfig, next?: AttrPlugin, @@ -257,7 +256,7 @@ function generateNodeAttrForRax( function generateEventHandlerAttrForRax( attrName: string, - attrValue: CompositeValue, + attrValue: IPublicTypeCompositeValue, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { diff --git a/modules/code-generator/src/plugins/component/react/containerClass.ts b/modules/code-generator/src/plugins/component/react/containerClass.ts index 0758c893e2..eab6cbebee 100644 --- a/modules/code-generator/src/plugins/component/react/containerClass.ts +++ b/modules/code-generator/src/plugins/component/react/containerClass.ts @@ -26,7 +26,7 @@ const pluginFactory: BuilderComponentPluginFactory = () => { // 将模块名转换成 PascalCase 的格式,并添加特定后缀,防止命名冲突 const componentClassName = ensureValidClassName( - `${changeCase.pascalCase(ir.moduleName)}$$Page`, + `${changeCase.pascalCase(ir.moduleName)}$$${ir.containerType}`, ); next.chunks.push({ @@ -43,6 +43,18 @@ const pluginFactory: BuilderComponentPluginFactory = () => { ], }); + if (ir.containerType === 'Component') { + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: CLASS_DEFINE_CHUNK_NAME.InsVar, + content: `static displayName = '${ir.moduleName}';`, + linkAfter: [ + CLASS_DEFINE_CHUNK_NAME.Start, + ], + }); + } + next.chunks.push({ type: ChunkType.STRING, fileType: FileType.JSX, diff --git a/modules/code-generator/src/plugins/component/react/containerInitState.ts b/modules/code-generator/src/plugins/component/react/containerInitState.ts index 5f246e1156..d1dd0d1ddf 100644 --- a/modules/code-generator/src/plugins/component/react/containerInitState.ts +++ b/modules/code-generator/src/plugins/component/react/containerInitState.ts @@ -13,12 +13,12 @@ import { } from '../../../types'; export interface PluginConfig { - fileType: string; + fileType?: string; implementType: 'inConstructor' | 'insMember' | 'hooks'; } const pluginFactory: BuilderComponentPluginFactory = (config?) => { - const cfg: PluginConfig = { + const cfg: PluginConfig & { fileType: string } = { fileType: FileType.JSX, implementType: 'inConstructor', ...config, diff --git a/modules/code-generator/src/plugins/component/react/containerInjectConstants.ts b/modules/code-generator/src/plugins/component/react/containerInjectConstants.ts new file mode 100644 index 0000000000..92cd4a3920 --- /dev/null +++ b/modules/code-generator/src/plugins/component/react/containerInjectConstants.ts @@ -0,0 +1,55 @@ +import { + CLASS_DEFINE_CHUNK_NAME, + COMMON_CHUNK_NAME, + DEFAULT_LINK_AFTER, +} from '../../../const/generator'; + +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, +} from '../../../types'; + +export interface PluginConfig { + fileType: string; +} + +const pluginFactory: BuilderComponentPluginFactory = (config?) => { + const cfg: PluginConfig = { + fileType: FileType.JSX, + ...config, + }; + + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: cfg.fileType, + name: COMMON_CHUNK_NAME.InternalDepsImport, + content: "import __$$constants from '../../constants';", + linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: cfg.fileType, + name: CLASS_DEFINE_CHUNK_NAME.InsVar, + content: ` + get constants() { + return __$$constants || {}; + } + `, + linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsVar]], + }); + + return next; + }; + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/component/react/containerInjectDataSourceEngine.ts b/modules/code-generator/src/plugins/component/react/containerInjectDataSourceEngine.ts index 20302dea99..c4385017b8 100644 --- a/modules/code-generator/src/plugins/component/react/containerInjectDataSourceEngine.ts +++ b/modules/code-generator/src/plugins/component/react/containerInjectDataSourceEngine.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/indent */ import { - CompositeValue, - JSExpression, + IPublicTypeCompositeValue, + IPublicTypeJSExpression, InterpretDataSourceConfig, isJSExpression, isJSFunction, @@ -27,8 +27,9 @@ import { import { generateCompositeType } from '../../../utils/compositeType'; import { parseExpressionConvertThis2Context } from '../../../utils/expressionParser'; -import { isContainerSchema } from '../../../utils/schema'; +import { isValidContainerType } from '../../../utils/schema'; import { REACT_CHUNK_NAME } from './const'; +import { isJSExpressionFn } from '../../../utils/common'; export interface PluginConfig { fileType?: string; @@ -37,6 +38,7 @@ export interface PluginConfig { * 数据源配置 */ datasourceConfig?: { + /** 数据源引擎的版本 */ engineVersion?: string; @@ -67,12 +69,12 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => }; const scope = Scope.createRootScope(); - const dataSourceConfig = isContainerSchema(pre.ir) ? pre.ir.dataSource : null; + const dataSourceConfig = isValidContainerType(pre.ir) ? pre.ir.dataSource : null; const dataSourceItems: InterpretDataSourceConfig[] = (dataSourceConfig && dataSourceConfig.list) || []; const dataSourceEngineOptions = { runtimeConfig: true }; if (dataSourceItems.length > 0) { - const requestHandlersMap: Record = {}; + const requestHandlersMap: Record = {}; dataSourceItems.forEach((ds) => { const dsType = ds.type || 'fetch'; @@ -187,16 +189,16 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => export default pluginFactory; -function wrapAsFunction(value: CompositeValue, scope: IScope): CompositeValue { - if (isJSExpression(value) || isJSFunction(value)) { +function wrapAsFunction(value: IPublicTypeCompositeValue, scope: IScope): IPublicTypeCompositeValue { + if (isJSExpression(value) || isJSFunction(value) || isJSExpressionFn(value)) { return { type: 'JSExpression', - value: `function(){ return ((${value.value}))}`, + value: `function(){ return ((${value.value}))}.bind(this)`, }; } return { type: 'JSExpression', - value: `function(){return((${generateCompositeType(value, scope)}))}`, + value: `function(){return((${generateCompositeType(value, scope)}))}.bind(this)`, }; } diff --git a/modules/code-generator/src/plugins/component/react/containerInjectI18n.ts b/modules/code-generator/src/plugins/component/react/containerInjectI18n.ts index da04029c00..aff42af15f 100644 --- a/modules/code-generator/src/plugins/component/react/containerInjectI18n.ts +++ b/modules/code-generator/src/plugins/component/react/containerInjectI18n.ts @@ -11,6 +11,7 @@ import { FileType, ICodeStruct, } from '../../../types'; +import { getSlotRelativePath } from '../../../utils/pathHelper'; export interface PluginConfig { fileType: string; @@ -31,9 +32,8 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => type: ChunkType.STRING, fileType: cfg.fileType, name: COMMON_CHUNK_NAME.InternalDepsImport, - // TODO: 下面这个路径有没有更好的方式来获取?而非写死 content: ` - import * as __$$i18n from '../../i18n'; + import * as __$$i18n from '${getSlotRelativePath({ contextData: next.contextData, from: 'components', to: 'i18n' })}'; `, linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], }); diff --git a/modules/code-generator/src/plugins/component/react/containerInjectUtils.ts b/modules/code-generator/src/plugins/component/react/containerInjectUtils.ts index 6c614f143a..d9eb14f769 100644 --- a/modules/code-generator/src/plugins/component/react/containerInjectUtils.ts +++ b/modules/code-generator/src/plugins/component/react/containerInjectUtils.ts @@ -12,13 +12,17 @@ import { ICodeStruct, IContainerInfo, } from '../../../types'; +import { getSlotRelativePath } from '../../../utils/pathHelper'; export interface PluginConfig { - fileType: string; + fileType?: string; + + /** prefer using class property to define utils */ + preferClassProperty?: boolean; } const pluginFactory: BuilderComponentPluginFactory = (config?) => { - const cfg: PluginConfig = { + const cfg: PluginConfig & { fileType: string } = { fileType: FileType.JSX, ...config, }; @@ -36,20 +40,31 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => type: ChunkType.STRING, fileType: cfg.fileType, name: COMMON_CHUNK_NAME.InternalDepsImport, - // TODO: 下面这个路径有没有更好的方式来获取?而非写死 content: ` - import utils${useRef ? ', { RefsManager }' : ''} from '../../utils'; + import utils${useRef ? ', { RefsManager }' : ''} from '${getSlotRelativePath({ contextData: next.contextData, from: 'components', to: 'utils' })}'; `, linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport], }); - next.chunks.push({ - type: ChunkType.STRING, - fileType: cfg.fileType, - name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent, - content: 'this.utils = utils;', - linkAfter: [CLASS_DEFINE_CHUNK_NAME.ConstructorStart], - }); + if (cfg.preferClassProperty) { + // mode: class property + next.chunks.push({ + type: ChunkType.STRING, + fileType: cfg.fileType, + name: CLASS_DEFINE_CHUNK_NAME.InsVar, + content: 'utils = utils;', + linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsVar]], + }); + } else { + // mode: assign in constructor + next.chunks.push({ + type: ChunkType.STRING, + fileType: cfg.fileType, + name: CLASS_DEFINE_CHUNK_NAME.ConstructorContent, + content: 'this.utils = utils;', + linkAfter: [CLASS_DEFINE_CHUNK_NAME.ConstructorStart], + }); + } if (useRef) { next.chunks.push({ @@ -89,7 +104,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => type: ChunkType.STRING, fileType: cfg.fileType, name: CLASS_DEFINE_CHUNK_NAME.InsMethod, - content: ` $ = () => null; `, + content: ' $ = () => null; ', linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]], }); @@ -97,7 +112,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => type: ChunkType.STRING, fileType: cfg.fileType, name: CLASS_DEFINE_CHUNK_NAME.InsMethod, - content: ` $$ = () => []; `, + content: ' $$ = () => []; ', linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]], }); } diff --git a/modules/code-generator/src/plugins/component/react/containerLifeCycle.ts b/modules/code-generator/src/plugins/component/react/containerLifeCycle.ts index 8caa9b105a..5f7d6d22a7 100644 --- a/modules/code-generator/src/plugins/component/react/containerLifeCycle.ts +++ b/modules/code-generator/src/plugins/component/react/containerLifeCycle.ts @@ -13,15 +13,17 @@ import { IContainerInfo, } from '../../../types'; import { isJSFunction, isJSExpression } from '@alilc/lowcode-types'; +import { isJSExpressionFn } from '../../../utils/common'; export interface PluginConfig { - fileType: string; - exportNameMapping: Record; - normalizeNameMapping: Record; + fileType?: string; + exportNameMapping?: Record; + normalizeNameMapping?: Record; + exclude?: string[]; } const pluginFactory: BuilderComponentPluginFactory = (config?) => { - const cfg: PluginConfig = { + const cfg = { fileType: FileType.JSX, exportNameMapping: {}, normalizeNameMapping: {}, @@ -41,6 +43,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 过滤掉非法数据(有些场景下会误传入空字符串或 null) if ( !isJSFunction(lifeCycles[lifeCycleName]) && + !isJSExpressionFn(lifeCycles[lifeCycleName]) && !isJSExpression(lifeCycles[lifeCycleName]) ) { return null; @@ -54,6 +57,10 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => normalizeName = cfg.normalizeNameMapping[lifeCycleName] || lifeCycleName; } + if (cfg?.exclude?.includes(normalizeName)) { + return null; + } + const exportName = cfg.exportNameMapping[lifeCycleName] || lifeCycleName; if (normalizeName === 'constructor') { return { @@ -95,7 +102,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => }), linkAfter: [...DEFAULT_LINK_AFTER[CLASS_DEFINE_CHUNK_NAME.InsMethod]], }; - }); + }).filter((i) => !!i); next.chunks.push(...chunks.filter((x): x is ICodeChunk => x !== null)); } diff --git a/modules/code-generator/src/plugins/component/react/jsx.ts b/modules/code-generator/src/plugins/component/react/jsx.ts index 445dce7a5f..588b356ad6 100644 --- a/modules/code-generator/src/plugins/component/react/jsx.ts +++ b/modules/code-generator/src/plugins/component/react/jsx.ts @@ -16,7 +16,7 @@ import { COMMON_CHUNK_NAME } from '../../../const/generator'; import { createReactNodeGenerator } from '../../../utils/nodeToJSX'; import { Scope } from '../../../utils/Scope'; -import { JSExpression } from '@alilc/lowcode-types'; +import { IPublicTypeJSExpression } from '@alilc/lowcode-types'; import { generateExpression } from '../../../utils/jsExpression'; import { transformJsExpr } from '../../../core/jsx/handlers/transformJsExpression'; import { transformThis2Context } from '../../../core/jsx/handlers/transformThis2Context'; @@ -47,7 +47,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => // 这里会将内部的一些子上下文的访问(this.xxx)转换为 __$$context.xxx 的形式 // 与 Rax 所不同的是,这里不会将最顶层的 this 转换掉 const customHandlers: HandlerSet = { - expression(input: JSExpression, scope: IScope, config) { + expression(input: IPublicTypeJSExpression, scope: IScope, config) { return transformJsExpr(generateExpression(input, scope), scope, { dontWrapEval: !(config?.tolerateEvalErrors ?? tolerateEvalErrors), dontTransformThis2ContextAtRootScope: true, @@ -58,7 +58,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => generateCompositeType( { type: 'JSFunction', - value: input.value || 'null', + value: input.value || 'function () {}', }, Scope.createRootScope(), ), @@ -120,7 +120,7 @@ const pluginFactory: BuilderComponentPluginFactory = (config?) => function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { ${evalErrorsHandler} } } diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/index.ts b/modules/code-generator/src/plugins/project/framework/icejs3/index.ts new file mode 100644 index 0000000000..37fa7a9e39 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/index.ts @@ -0,0 +1,17 @@ +import template from './template'; +import globalStyle from './plugins/globalStyle'; +import packageJSON from './plugins/packageJSON'; +import layout from './plugins/layout'; +import appConfig from './plugins/appConfig'; +import buildConfig from './plugins/buildConfig'; + +export default { + template, + plugins: { + appConfig, + buildConfig, + globalStyle, + packageJSON, + layout, + }, +}; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/plugins/appConfig.ts b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/appConfig.ts new file mode 100644 index 0000000000..6ec5384a5d --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/appConfig.ts @@ -0,0 +1,50 @@ +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, +} from '../../../../../types'; +import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; + +export interface AppConfigPluginConfig { + +} + +function getContent() { + return `import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +}));`; +} + +const pluginFactory: BuilderComponentPluginFactory = () => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.TS, + name: COMMON_CHUNK_NAME.FileMainContent, + content: getContent(), + linkAfter: [], + }); + + return next; + }; + + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/plugins/buildConfig.ts b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/buildConfig.ts new file mode 100644 index 0000000000..322796154a --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/buildConfig.ts @@ -0,0 +1,165 @@ +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, +} from '../../../../../types'; +import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; +import { format } from '../../../../../utils/format'; +import { getThemeInfo } from '../../../../../utils/theme'; + +export interface BuildConfigPluginConfig { + + /** 包名 */ + themePackage?: string; +} + +function getContent(cfg?: BuildConfigPluginConfig, routesContent?: string) { + return ` +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + } + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = \` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + \`; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + ${routesContent} + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion(${cfg?.themePackage ? `{ + importStyle: 'sass', + themePackage: '${getThemeInfo(cfg.themePackage).name}', + }` : `{ + importStyle: 'sass', + }`}), + locales(), + plugin(), + ] +})); + `; +} + +function getRoutesContent(navData: any, needShell = true) { + const routes = [ + 'routes: {', + ' defineRoutes: route => {', + ]; + function _getRoutes(nav: any, _routes: string[] = []) { + const { slug, children } = nav; + if (children && children.length > 0) { + children.forEach((_nav: any) => _getRoutes(_nav, _routes)); + } else if (slug) { + _routes.push(`route('/${slug}', '${slug}/index.jsx');`); + } + } + if (needShell) { + routes.push(" route('/', 'layout.jsx', () => {"); + } + navData?.forEach((nav: any) => { + _getRoutes(nav, routes); + }); + if (needShell) { + routes.push(' });'); + } + routes.push(' }'); // end of defineRoutes + routes.push(' },'); // end of routes + return routes.join('\n'); +} + +const pluginFactory: BuilderComponentPluginFactory = (cfg?) => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const { navConfig } = next.contextData; + const routesContent = navConfig?.data ? getRoutesContent(navConfig.data, true) : ''; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.MTS, + name: COMMON_CHUNK_NAME.FileMainContent, + content: format(getContent(cfg, routesContent)), + linkAfter: [], + }); + + return next; + }; + + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/plugins/globalStyle.ts b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/globalStyle.ts new file mode 100644 index 0000000000..3daaeecdc0 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/globalStyle.ts @@ -0,0 +1,56 @@ +import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; + +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '../../../../../types'; + +const pluginFactory: BuilderComponentPluginFactory = () => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.SCSS, + name: COMMON_CHUNK_NAME.StyleDepsImport, + content: ` + // 引入默认全局样式 + @import '@alifd/next/reset.scss'; + `, + linkAfter: [], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.SCSS, + name: COMMON_CHUNK_NAME.StyleCssContent, + content: ` + body { + -webkit-font-smoothing: antialiased; + } + `, + linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport], + }); + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.SCSS, + name: COMMON_CHUNK_NAME.StyleCssContent, + content: ir.css || '', + linkAfter: [COMMON_CHUNK_NAME.StyleDepsImport], + }); + + return next; + }; + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/plugins/layout.ts b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/layout.ts new file mode 100644 index 0000000000..a67f227019 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/layout.ts @@ -0,0 +1,41 @@ +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, +} from '../../../../../types'; +import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; + +const pluginFactory: BuilderComponentPluginFactory = () => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + next.chunks.push({ + type: ChunkType.STRING, + fileType: FileType.JSX, + name: COMMON_CHUNK_NAME.FileMainContent, + content: ` + import { Outlet } from 'ice'; + import BasicLayout from '@/layouts/BasicLayout'; + + export default function Layout() { + return ( + + + + );; + } + `, + linkAfter: [], + }); + + return next; + }; + + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/plugins/packageJSON.ts b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/packageJSON.ts new file mode 100644 index 0000000000..3c39ba7399 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/plugins/packageJSON.ts @@ -0,0 +1,119 @@ +import { PackageJSON } from '@alilc/lowcode-types'; + +import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; + +import { + BuilderComponentPlugin, + BuilderComponentPluginFactory, + ChunkType, + FileType, + ICodeStruct, + IProjectInfo, +} from '../../../../../types'; +import { buildDataSourceDependencies } from '../../../../../utils/dataSource'; + +interface IIceJs3PackageJSON extends PackageJSON { + originTemplate: string; +} + +export type IceJs3PackageJsonPluginConfig = { + + /** + * 数据源配置 + */ + datasourceConfig?: { + + /** 数据源引擎的版本 */ + engineVersion?: string; + + /** 数据源引擎的包名 */ + enginePackage?: string; + + /** 数据源 handlers 的版本 */ + handlersVersion?: { + [key: string]: string; + }; + + /** 数据源 handlers 的包名 */ + handlersPackages?: { + [key: string]: string; + }; + }; + + /** 包名 */ + packageName?: string; + + /** 版本 */ + packageVersion?: string; +}; + +const pluginFactory: BuilderComponentPluginFactory = (cfg) => { + const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => { + const next: ICodeStruct = { + ...pre, + }; + + const ir = next.ir as IProjectInfo; + + const packageJson: IIceJs3PackageJSON = { + name: cfg?.packageName || 'icejs3-demo-app', + version: cfg?.packageVersion || '0.1.5', + description: 'icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。', + dependencies: { + moment: '^2.24.0', + react: '^18.2.0', + 'react-dom': '^18.2.0', + 'react-router': '^6.9.0', + 'react-router-dom': '^6.9.0', + 'intl-messageformat': '^9.3.6', + '@alifd/next': '1.26.15', + '@ice/runtime': '~1.1.0', + // 数据源相关的依赖: + ...buildDataSourceDependencies(ir, cfg?.datasourceConfig), + }, + devDependencies: { + '@ice/app': '~3.1.0', + '@types/react': '^18.0.0', + '@types/react-dom': '^18.0.0', + '@types/node': '^18.11.17', + '@ice/plugin-fusion': '^1.0.1', + '@ice/plugin-moment-locales': '^1.0.0', + eslint: '^6.0.1', + stylelint: '^13.2.0', + }, + scripts: { + start: 'ice start', + build: 'ice build', + lint: 'npm run eslint && npm run stylelint', + eslint: 'eslint --cache --ext .js,.jsx ./', + stylelint: 'stylelint ./**/*.scss', + }, + engines: { + node: '>=14.0.0', + }, + repository: { + type: 'git', + url: 'http://gitlab.xxx.com/msd/leak-scan/tree/master', + }, + private: true, + originTemplate: '@alifd/scaffold-lite-js', + }; + + ir.packages.forEach((packageInfo) => { + packageJson.dependencies[packageInfo.package] = packageInfo.version; + }); + + next.chunks.push({ + type: ChunkType.JSON, + fileType: FileType.JSON, + name: COMMON_CHUNK_NAME.FileMainContent, + content: packageJson, + linkAfter: [], + }); + + return next; + }; + return plugin; +}; + +export default pluginFactory; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/README.md.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/README.md.ts new file mode 100644 index 0000000000..e73ab2b82c --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/README.md.ts @@ -0,0 +1,12 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'README', + 'md', + 'This project is generated by lowcode-code-generator & lowcode-solution-icejs3.', + ); + + return [[], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/browserslistrc.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/browserslistrc.ts new file mode 100644 index 0000000000..a3a346e3ff --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/browserslistrc.ts @@ -0,0 +1,14 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + '.browserslistrc', + '', + `defaults +ios_saf 9 + `, + ); + + return [[], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/document.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/document.ts new file mode 100644 index 0000000000..3345625557 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/document.ts @@ -0,0 +1,41 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +/* eslint-disable max-len */ +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'document', + 'tsx', + `import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + + + + + + + + + + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +}`, + ); + + return [['src'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/gitignore.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/gitignore.ts new file mode 100644 index 0000000000..5c6eb3f13b --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/gitignore.ts @@ -0,0 +1,36 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + '.gitignore', + '', + ` +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + `, + ); + + return [[], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts new file mode 100644 index 0000000000..9647a76439 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.jsx.ts @@ -0,0 +1,25 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'jsx', + ` +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Footer'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.style.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.style.ts new file mode 100644 index 0000000000..941be0d263 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Footer/index.style.ts @@ -0,0 +1,26 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'module.scss', + ` +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Footer'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts new file mode 100644 index 0000000000..9c078c92c2 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.jsx.ts @@ -0,0 +1,27 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'jsx', + ` +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Logo'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.style.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.style.ts new file mode 100644 index 0000000000..dfd00dd3e0 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/Logo/index.style.ts @@ -0,0 +1,31 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'module.scss', + ` +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'Logo'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts new file mode 100644 index 0000000000..1713057566 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/components/PageNav/index.jsx.ts @@ -0,0 +1,79 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'jsx', + `import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + `, + ); + + return [['src', 'layouts', 'BasicLayout', 'components', 'PageNav'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/index.jsx.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/index.jsx.ts new file mode 100644 index 0000000000..9c7318dab0 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/index.jsx.ts @@ -0,0 +1,92 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'index', + 'jsx', + ` +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + `, + ); + + return [['src', 'layouts', 'BasicLayout'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/menuConfig.js.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/menuConfig.js.ts new file mode 100644 index 0000000000..636539b657 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/src/layouts/BasicLayout/menuConfig.js.ts @@ -0,0 +1,22 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'menuConfig', + 'js', + ` +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + `, + ); + + return [['src', 'layouts', 'BasicLayout'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/tsconfig.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/tsconfig.ts new file mode 100644 index 0000000000..f58639db20 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/tsconfig.ts @@ -0,0 +1,38 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'tsconfig', + 'json', + ` +{ + "compilerOptions": { + "baseUrl": "./", + "module": "ESNext", + "target": "ESNext", + "lib": ["DOM", "ESNext", "DOM.Iterable"], + "jsx": "react-jsx", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noImplicitAny": false, + "importHelpers": true, + "strictNullChecks": true, + "suppressImplicitAnyIndexErrors": true, + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"], + "ice": [".ice"] + } + }, + "include": ["src", ".ice"], + "exclude": ["build"] +} + `, + ); + + return [[], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/files/typings.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/typings.ts new file mode 100644 index 0000000000..c8cbe92545 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/files/typings.ts @@ -0,0 +1,20 @@ +import { ResultFile } from '@alilc/lowcode-types'; +import { createResultFile } from '../../../../../../utils/resultHelper'; + +export default function getFile(): [string[], ResultFile] { + const file = createResultFile( + 'typings.d', + 'ts', + `/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + `, + ); + + return [['src'], file]; +} diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/index.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/index.ts new file mode 100644 index 0000000000..e29f931386 --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/index.ts @@ -0,0 +1,57 @@ +import { IProjectTemplate } from '../../../../../types'; +import { generateStaticFiles } from './static-files'; + +const icejs3Template: IProjectTemplate = { + slots: { + components: { + path: ['src', 'components'], + fileName: 'index', + }, + pages: { + path: ['src', 'pages'], + fileName: 'index', + }, + entry: { + path: ['src'], + fileName: 'app', + }, + constants: { + path: ['src'], + fileName: 'constants', + }, + utils: { + path: ['src'], + fileName: 'utils', + }, + i18n: { + path: ['src'], + fileName: 'i18n', + }, + globalStyle: { + path: ['src'], + fileName: 'global', + }, + packageJSON: { + path: [], + fileName: 'package', + }, + appConfig: { + path: ['src'], + fileName: 'app', + }, + buildConfig: { + path: [], + fileName: 'ice.config', + }, + layout: { + path: ['src', 'pages'], + fileName: 'layout', + }, + }, + + generateTemplate() { + return generateStaticFiles(); + }, +}; + +export default icejs3Template; diff --git a/modules/code-generator/src/plugins/project/framework/icejs3/template/static-files.ts b/modules/code-generator/src/plugins/project/framework/icejs3/template/static-files.ts new file mode 100644 index 0000000000..8f3794b61a --- /dev/null +++ b/modules/code-generator/src/plugins/project/framework/icejs3/template/static-files.ts @@ -0,0 +1,33 @@ +import { ResultDir } from '@alilc/lowcode-types'; +import { createResultDir } from '../../../../../utils/resultHelper'; +import { runFileGenerator } from '../../../../../utils/templateHelper'; + +import file1 from './files/gitignore'; +import file2 from './files/README.md'; +import file3 from './files/browserslistrc'; +import file4 from './files/typings'; +import file5 from './files/document'; +import file6 from './files/src/layouts/BasicLayout/components/Footer/index.jsx'; +import file7 from './files/src/layouts/BasicLayout/components/Footer/index.style'; +import file8 from './files/src/layouts/BasicLayout/components/Logo/index.jsx'; +import file9 from './files/src/layouts/BasicLayout/components/Logo/index.style'; +import file10 from './files/src/layouts/BasicLayout/components/PageNav/index.jsx'; +import file11 from './files/src/layouts/BasicLayout/index.jsx'; +import file12 from './files/src/layouts/BasicLayout/menuConfig.js'; + +export function generateStaticFiles(root = createResultDir('.')): ResultDir { + runFileGenerator(root, file1); + runFileGenerator(root, file2); + runFileGenerator(root, file3); + runFileGenerator(root, file4); + runFileGenerator(root, file5); + runFileGenerator(root, file6); + runFileGenerator(root, file7); + runFileGenerator(root, file8); + runFileGenerator(root, file9); + runFileGenerator(root, file10); + runFileGenerator(root, file11); + runFileGenerator(root, file12); + + return root; +} diff --git a/modules/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts b/modules/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts index e95a3dc0bf..401ba3f97f 100644 --- a/modules/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts +++ b/modules/code-generator/src/plugins/project/framework/rax/plugins/packageJSON.ts @@ -1,4 +1,4 @@ -import { NpmInfo, PackageJSON } from '@alilc/lowcode-types'; +import { IPublicTypeNpmInfo, PackageJSON } from '@alilc/lowcode-types'; import { COMMON_CHUNK_NAME } from '../../../../../const/generator'; import { @@ -84,9 +84,9 @@ const pluginFactory: BuilderComponentPluginFactory<RaxFrameworkOptions> = (cfg) export default pluginFactory; -function getNpmDependencies(project: IProjectInfo): NpmInfo[] { - const npmDeps: NpmInfo[] = []; - const npmNameToPkgMap = new Map<string, NpmInfo>(); +function getNpmDependencies(project: IProjectInfo): IPublicTypeNpmInfo[] { + const npmDeps: IPublicTypeNpmInfo[] = []; + const npmNameToPkgMap = new Map<string, IPublicTypeNpmInfo>(); const allDeps = project.packages; diff --git a/modules/code-generator/src/plugins/project/i18n.ts b/modules/code-generator/src/plugins/project/i18n.ts index 7658f9c854..4c36345a5f 100644 --- a/modules/code-generator/src/plugins/project/i18n.ts +++ b/modules/code-generator/src/plugins/project/i18n.ts @@ -34,14 +34,14 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => { }; const isEmptyVariables = variables => ( - Array.isArray(variables) && variables.length === 0 + Array.isArray(variables) && variables.length === 0 || typeof variables === 'object' && (!variables || Object.keys(variables).length === 0) ); // 按低代码规范里面的要求进行变量替换 const format = (msg, variables) => ( - typeof msg === 'string' - ? msg.replace(/\\\$\\{(\\w+)\\}/g, (match, key) => variables?.[key] ?? '') + typeof msg === 'string' + ? msg.replace(/\\\$?\\{(\\w+)\\}/g, (match, key) => variables?.[key] ?? '') : msg ); @@ -69,7 +69,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => { }; target._i18nText = (t) => { // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')] + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')] if (localMsg != null) { return format(localMsg, t.params); } @@ -81,7 +81,7 @@ const pluginFactory: BuilderComponentPluginFactory<unknown> = () => { } // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); + return format(t[t.use || "zh-CN"] ?? t.en_US, t.params); } // 注入到上下文中去 diff --git a/modules/code-generator/src/postprocessor/prettier/index.ts b/modules/code-generator/src/postprocessor/prettier/index.ts index afdcc62258..079c45582e 100644 --- a/modules/code-generator/src/postprocessor/prettier/index.ts +++ b/modules/code-generator/src/postprocessor/prettier/index.ts @@ -9,7 +9,7 @@ const PARSERS = ['css', 'scss', 'less', 'json', 'html', 'vue']; export interface ProcessorConfig { customFileTypeParser: Record<string, string>; - plugins?: Array<prettier.Plugin>; + plugins?: prettier.Plugin[]; } const factory: PostProcessorFactory<ProcessorConfig> = (config?: ProcessorConfig) => { @@ -20,8 +20,10 @@ const factory: PostProcessorFactory<ProcessorConfig> = (config?: ProcessorConfig const codePrettier: PostProcessor = (content: string, fileType: string) => { let parser: prettier.BuiltInParserName | any; - if (fileType === 'js' || fileType === 'jsx') { - parser = 'babel'; + if (fileType === 'js' || fileType === 'jsx' || fileType === 'ts' || fileType === 'tsx') { + parser = 'babel-ts'; + } else if (fileType === 'json') { + parser = 'json-stringify'; } else if (PARSERS.indexOf(fileType) >= 0) { parser = fileType; } else if (cfg.customFileTypeParser[fileType]) { @@ -33,6 +35,8 @@ const factory: PostProcessorFactory<ProcessorConfig> = (config?: ProcessorConfig return prettier.format(content, { parser, plugins: [parserBabel, parserPostCss, parserHtml, ...(cfg.plugins || [])], + singleQuote: true, + jsxSingleQuote: false, }); }; diff --git a/modules/code-generator/src/publisher/zip/index.ts b/modules/code-generator/src/publisher/zip/index.ts index cc2f082b27..0ac0b6f67f 100644 --- a/modules/code-generator/src/publisher/zip/index.ts +++ b/modules/code-generator/src/publisher/zip/index.ts @@ -2,9 +2,9 @@ import { ResultDir } from '@alilc/lowcode-types'; import { PublisherFactory, IPublisher, IPublisherFactoryParams, PublisherError } from '../../types'; import { getErrorMessage } from '../../utils/errors'; import { isNodeProcess, writeZipToDisk, generateProjectZip } from './utils'; +import { saveAs } from 'file-saver'; -// export type ZipBuffer = Buffer | Blob; -export type ZipBuffer = Buffer; +export type ZipBuffer = Buffer | Blob; declare type ZipPublisherResponse = string | ZipBuffer; @@ -44,10 +44,16 @@ export const createZipPublisher: PublisherFactory<ZipFactoryParams, ZipPublisher try { const zipContent = await generateProjectZip(projectToPublish); - // If not output path is provided, zip is not written to disk - const projectOutputPath = options.outputPath || outputPath; - if (projectOutputPath && isNodeProcess()) { - await writeZipToDisk(projectOutputPath, zipContent, zipName); + if (isNodeProcess()) { + // If not output path is provided on the node side, zip is not written to disk + const projectOutputPath = options.outputPath || outputPath; + if (projectOutputPath) { + await writeZipToDisk(projectOutputPath, zipContent, zipName); + } + } else { + // the browser end does not require a path + // auto download zip files + saveAs(zipContent as Blob, `${zipName}.zip`); } return { success: true, payload: zipContent }; diff --git a/modules/code-generator/src/publisher/zip/utils.ts b/modules/code-generator/src/publisher/zip/utils.ts index 10e1ad4a68..08d3a12f44 100644 --- a/modules/code-generator/src/publisher/zip/utils.ts +++ b/modules/code-generator/src/publisher/zip/utils.ts @@ -40,8 +40,7 @@ export const writeZipToDisk = ( export const generateProjectZip = async (project: ResultDir): Promise<ZipBuffer> => { let zip = new JSZip(); zip = writeFolderToZip(project, zip, true); - // const zipType = isNodeProcess() ? 'nodebuffer' : 'blob'; - const zipType = 'nodebuffer'; // 目前先只支持 node 调用 + const zipType = isNodeProcess() ? 'nodebuffer' : 'blob'; return zip.generateAsync({ type: zipType }); }; diff --git a/modules/code-generator/src/solutions/icejs.ts b/modules/code-generator/src/solutions/icejs.ts index 0a9d5c60d1..1149098ecb 100644 --- a/modules/code-generator/src/solutions/icejs.ts +++ b/modules/code-generator/src/solutions/icejs.ts @@ -3,11 +3,13 @@ import { IProjectBuilder, IProjectBuilderOptions } from '../types'; import { createProjectBuilder } from '../generator/ProjectBuilder'; import esmodule from '../plugins/common/esmodule'; +import styleImport from '../plugins/common/styleImport'; import containerClass from '../plugins/component/react/containerClass'; import containerInitState from '../plugins/component/react/containerInitState'; import containerInjectContext from '../plugins/component/react/containerInjectContext'; import containerInjectUtils from '../plugins/component/react/containerInjectUtils'; import containerInjectDataSourceEngine from '../plugins/component/react/containerInjectDataSourceEngine'; +import containerInjectConstants from '../plugins/component/react/containerInjectConstants'; import containerInjectI18n from '../plugins/component/react/containerInjectI18n'; import containerLifeCycle from '../plugins/component/react/containerLifeCycle'; import containerMethod from '../plugins/component/react/containerMethod'; @@ -22,7 +24,7 @@ import icejs from '../plugins/project/framework/icejs'; import { prettier } from '../postprocessor'; -export interface IceJsProjectBuilderOptions extends IProjectBuilderOptions {} +export type IceJsProjectBuilderOptions = IProjectBuilderOptions; export default function createIceJsProjectBuilder( options?: IceJsProjectBuilderOptions, @@ -37,11 +39,13 @@ export default function createIceJsProjectBuilder( esmodule({ fileType: 'jsx', }), + styleImport(), containerClass(), containerInjectContext(), containerInjectUtils(), containerInjectDataSourceEngine(), containerInjectI18n(), + containerInjectConstants(), containerInitState(), containerLifeCycle(), containerMethod(), @@ -60,11 +64,13 @@ export default function createIceJsProjectBuilder( esmodule({ fileType: 'jsx', }), + styleImport(), containerClass(), containerInjectContext(), containerInjectUtils(), containerInjectDataSourceEngine(), containerInjectI18n(), + containerInjectConstants(), containerInitState(), containerLifeCycle(), containerMethod(), @@ -89,16 +95,18 @@ export default function createIceJsProjectBuilder( packageJSON: [icejs.plugins.packageJSON()], }, postProcessors: [prettier()], + customizeBuilderOptions: options?.customizeBuilderOptions, }); } export const plugins = { containerClass, - containerInitState, containerInjectContext, containerInjectUtils, - containerInjectI18n, containerInjectDataSourceEngine, + containerInjectI18n, + containerInjectConstants, + containerInitState, containerLifeCycle, containerMethod, jsx, diff --git a/modules/code-generator/src/solutions/icejs3.ts b/modules/code-generator/src/solutions/icejs3.ts new file mode 100644 index 0000000000..a8622e74ab --- /dev/null +++ b/modules/code-generator/src/solutions/icejs3.ts @@ -0,0 +1,113 @@ +import { IProjectBuilder, IProjectBuilderOptions } from '../types'; + +import { createProjectBuilder } from '../generator/ProjectBuilder'; + +import esmodule from '../plugins/common/esmodule'; +import styleImport from '../plugins/common/styleImport'; +import containerClass from '../plugins/component/react/containerClass'; +import containerInitState from '../plugins/component/react/containerInitState'; +import containerInjectContext from '../plugins/component/react/containerInjectContext'; +import containerInjectUtils from '../plugins/component/react/containerInjectUtils'; +import containerInjectDataSourceEngine from '../plugins/component/react/containerInjectDataSourceEngine'; +import containerInjectConstants from '../plugins/component/react/containerInjectConstants'; +import containerInjectI18n from '../plugins/component/react/containerInjectI18n'; +import containerLifeCycle from '../plugins/component/react/containerLifeCycle'; +import containerMethod from '../plugins/component/react/containerMethod'; +import jsx from '../plugins/component/react/jsx'; +import reactCommonDeps from '../plugins/component/react/reactCommonDeps'; +import css from '../plugins/component/style/css'; +import constants from '../plugins/project/constants'; +import i18n from '../plugins/project/i18n'; +import utils from '../plugins/project/utils'; + +import icejs3 from '../plugins/project/framework/icejs3'; + +import { prettier } from '../postprocessor'; + +export type IceJs3ProjectBuilderOptions = IProjectBuilderOptions; + +export default function createIceJsProjectBuilder( + options?: IceJs3ProjectBuilderOptions, +): IProjectBuilder { + return createProjectBuilder({ + inStrictMode: options?.inStrictMode, + extraContextData: { ...options }, + template: icejs3.template, + plugins: { + components: [ + reactCommonDeps(), + esmodule({ + fileType: 'jsx', + }), + styleImport(), + containerClass(), + containerInjectContext(), + containerInjectUtils(), + containerInjectDataSourceEngine(), + containerInjectI18n(), + containerInjectConstants(), + containerInitState(), + containerLifeCycle(), + containerMethod(), + jsx({ + nodeTypeMapping: { + Div: 'div', + Component: 'div', + Page: 'div', + Block: 'div', + }, + }), + css(), + ], + pages: [ + reactCommonDeps(), + esmodule({ + fileType: 'jsx', + }), + styleImport(), + containerClass(), + containerInjectContext(), + containerInjectUtils(), + containerInjectDataSourceEngine(), + containerInjectI18n(), + containerInjectConstants(), + containerInitState(), + containerLifeCycle(), + containerMethod(), + jsx({ + nodeTypeMapping: { + Div: 'div', + Component: 'div', + Page: 'div', + Block: 'div', + Box: 'div', + }, + }), + css(), + ], + constants: [constants()], + utils: [esmodule(), utils('react')], + i18n: [i18n()], + globalStyle: [icejs3.plugins.globalStyle()], + packageJSON: [icejs3.plugins.packageJSON()], + buildConfig: [icejs3.plugins.buildConfig()], + appConfig: [icejs3.plugins.appConfig()], + layout: [icejs3.plugins.layout()], + }, + postProcessors: [prettier()], + customizeBuilderOptions: options?.customizeBuilderOptions, + }); +} + +export const plugins = { + containerClass, + containerInitState, + containerInjectContext, + containerInjectUtils, + containerInjectI18n, + containerInjectDataSourceEngine, + containerLifeCycle, + containerMethod, + jsx, + commonDeps: reactCommonDeps, +}; diff --git a/modules/code-generator/src/solutions/rax-app.ts b/modules/code-generator/src/solutions/rax-app.ts index c2b3adac50..f7e9038353 100644 --- a/modules/code-generator/src/solutions/rax-app.ts +++ b/modules/code-generator/src/solutions/rax-app.ts @@ -71,6 +71,7 @@ export default function createRaxProjectBuilder( packageJSON: [raxApp.plugins.packageJSON(options)], }, postProcessors: [prettier()], + customizeBuilderOptions: options?.customizeBuilderOptions, }); } diff --git a/modules/code-generator/src/standalone-loader.ts b/modules/code-generator/src/standalone-loader.ts index 882b1de9dd..0c8903ddcb 100644 --- a/modules/code-generator/src/standalone-loader.ts +++ b/modules/code-generator/src/standalone-loader.ts @@ -1,5 +1,5 @@ import fetch from 'node-fetch'; -import type { ProjectSchema, ResultDir } from '@alilc/lowcode-types'; +import type { IPublicTypeProjectSchema, ResultDir } from '@alilc/lowcode-types'; import type { FlattenFile } from './types/file'; declare const Worker: any; @@ -26,7 +26,7 @@ export type Result = ResultDir | FlattenFile[]; export async function generateCode(options: { solution: 'icejs' | 'rax'; - schema: ProjectSchema; + schema: IPublicTypeProjectSchema; flattenResult?: boolean; workerJsUrl?: string; timeoutInMs?: number; diff --git a/modules/code-generator/src/standalone-worker.ts b/modules/code-generator/src/standalone-worker.ts index 71f56e67a3..e00a4877bf 100644 --- a/modules/code-generator/src/standalone-worker.ts +++ b/modules/code-generator/src/standalone-worker.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import type { ProjectSchema } from '@alilc/lowcode-types'; +import type { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import CodeGen from './standalone'; declare const self: any; @@ -13,7 +13,7 @@ self.onmessage = (event: any) => { self.postMessage({ type: 'ready' }); -async function run(msg: { solution: string; schema: ProjectSchema; flattenResult?: boolean }) { +async function run(msg: { solution: string; schema: IPublicTypeProjectSchema; flattenResult?: boolean }) { try { print('begin run...'); self.postMessage({ type: 'run:begin' }); diff --git a/modules/code-generator/src/standalone.ts b/modules/code-generator/src/standalone.ts index 2f15515cf8..23d16e61f0 100644 --- a/modules/code-generator/src/standalone.ts +++ b/modules/code-generator/src/standalone.ts @@ -8,7 +8,8 @@ import './polyfills/buffer'; import { createProjectBuilder } from './generator/ProjectBuilder'; import { createModuleBuilder } from './generator/ModuleBuilder'; import { createZipPublisher } from './publisher/zip'; -import createIceJsProjectBuilder, { plugins as reactPlugins } from './solutions/icejs'; +import createIceJsProjectBuilder, { plugins as icejsPlugins } from './solutions/icejs'; +import createIceJs3ProjectBuilder, { plugins as icejs3Plugins } from './solutions/icejs3'; import createRaxAppProjectBuilder, { plugins as raxPlugins } from './solutions/rax-app'; // 引入说明 @@ -18,6 +19,7 @@ import { COMMON_CHUNK_NAME, CLASS_DEFINE_CHUNK_NAME, DEFAULT_LINK_AFTER } from ' // 引入通用插件组 import esmodule from './plugins/common/esmodule'; import requireUtils from './plugins/common/requireUtils'; +import styleImport from './plugins/common/styleImport'; import css from './plugins/component/style/css'; import constants from './plugins/project/constants'; @@ -32,6 +34,7 @@ import * as CONSTANTS from './const'; // 引入内置解决方案模块 import icejs from './plugins/project/framework/icejs'; +import icejs3 from './plugins/project/framework/icejs3'; import rax from './plugins/project/framework/rax'; export default { @@ -39,10 +42,12 @@ export default { createModuleBuilder, solutions: { icejs: createIceJsProjectBuilder, + icejs3: createIceJs3ProjectBuilder, rax: createRaxAppProjectBuilder, }, solutionParts: { icejs, + icejs3, rax, }, publishers: { @@ -50,6 +55,7 @@ export default { }, plugins: { common: { + /** * 处理 ES Module * @deprecated please use esModule @@ -57,12 +63,7 @@ export default { esmodule, esModule: esmodule, requireUtils, - }, - react: { - ...reactPlugins, - }, - rax: { - ...raxPlugins, + styleImport, }, style: { css, @@ -72,6 +73,22 @@ export default { i18n, utils, }, + icejs: { + ...icejsPlugins, + }, + icejs3: { + ...icejs3Plugins, + }, + rax: { + ...raxPlugins, + }, + + /** + * @deprecated please use icejs + */ + react: { + ...icejsPlugins, + }, }, postprocessor: { prettier, diff --git a/modules/code-generator/src/types/analyze.ts b/modules/code-generator/src/types/analyze.ts index 43de307434..3bf444d29e 100644 --- a/modules/code-generator/src/types/analyze.ts +++ b/modules/code-generator/src/types/analyze.ts @@ -1,7 +1,7 @@ -import type { ContainerSchema } from '@alilc/lowcode-types'; +import type { IPublicTypeContainerSchema } from '@alilc/lowcode-types'; export interface ICompAnalyzeResult { isUsingRef: boolean; } -export type TComponentAnalyzer = (container: ContainerSchema) => ICompAnalyzeResult; +export type TComponentAnalyzer = (container: IPublicTypeContainerSchema) => ICompAnalyzeResult; diff --git a/modules/code-generator/src/types/core.ts b/modules/code-generator/src/types/core.ts index 99ca8c97ad..1219a1e767 100644 --- a/modules/code-generator/src/types/core.ts +++ b/modules/code-generator/src/types/core.ts @@ -1,19 +1,15 @@ import { - JSONArray, - JSONObject, - CompositeArray, - CompositeObject, - ResultDir, + IPublicTypeCompositeArray, + IPublicTypeCompositeObject, IPublicTypeJSExpression, + IPublicTypeJSFunction, IPublicTypeJSONArray, + IPublicTypeJSONObject, IPublicTypeJSSlot, IPublicTypeNodeDataType, + IPublicTypeProjectSchema, ResultDir, ResultFile, - NodeDataType, - ProjectSchema, - JSExpression, - JSFunction, - JSSlot, } from '@alilc/lowcode-types'; -import { IParseResult } from './intermediate'; +import type { ProjectBuilderInitOptions } from '../generator/ProjectBuilder'; import { IScopeBindings } from '../utils/ScopeBindings'; +import { IParseResult } from './intermediate'; export enum FileType { CSS = 'css', @@ -21,10 +17,13 @@ export enum FileType { LESS = 'less', HTML = 'html', JS = 'js', + MJS = 'mjs', JSX = 'jsx', TS = 'ts', + MTS = 'mts', TSX = 'tsx', JSON = 'json', + MD = 'md', } export enum ChunkType { @@ -64,14 +63,17 @@ export interface ICodeStruct extends IBaseCodeStruct { /** 上下文数据,用来在插件之间共享一些数据 */ export interface IContextData extends IProjectBuilderOptions { - /** 是否使用了 Ref 的 API (this.$/this.$$) */ - useRefApi?: boolean; /** * 其他自定义数据 * (三方自定义插件也可以在此放一些数据,建议起个长一点的名称,用自己的插件名做前缀,以防冲突) */ [key: string]: any; + + /** + * 是否使用了 Ref 的 API (this.$/this.$$) + * */ + useRefApi?: boolean; } export type BuilderComponentPlugin = (initStruct: ICodeStruct) => Promise<ICodeStruct>; @@ -95,7 +97,7 @@ export interface ICompiledModule { export interface IModuleBuilder { generateModule: (input: unknown) => Promise<ICompiledModule>; - generateModuleCode: (schema: ProjectSchema | string) => Promise<ResultDir>; + generateModuleCode: (schema: IPublicTypeProjectSchema | string) => Promise<ResultDir>; linkCodeChunks: (chunks: Record<string, ICodeChunk[]>, fileName: string) => ResultFile[]; addPlugin: (plugin: BuilderComponentPlugin) => void; } @@ -107,24 +109,25 @@ export interface IModuleBuilder { * @interface ICodeGenerator */ export interface ICodeGenerator { + /** * 出码接口,把 Schema 转换成代码文件系统描述 * - * @param {(ProjectSchema)} schema 传入的 Schema + * @param {(IPublicTypeProjectSchema)} schema 传入的 Schema * @returns {ResultDir} * @memberof ICodeGenerator */ - toCode: (schema: ProjectSchema) => Promise<ResultDir>; + toCode: (schema: IPublicTypeProjectSchema) => Promise<ResultDir>; } export interface ISchemaParser { - validate: (schema: ProjectSchema) => boolean; - parse: (schema: ProjectSchema | string) => IParseResult; + validate: (schema: IPublicTypeProjectSchema) => boolean; + parse: (schema: IPublicTypeProjectSchema | string) => IParseResult; } export interface IProjectTemplate { slots: Record<string, IProjectSlot>; - generateTemplate: () => ResultDir | Promise<ResultDir>; + generateTemplate: (data: IParseResult) => ResultDir | Promise<ResultDir>; } export interface IProjectSlot { @@ -137,51 +140,65 @@ export interface IProjectPlugins { } export interface IProjectBuilderOptions { - /** 是否处于严格模式(默认: 否) */ + + /** 是否处于严格模式 (默认:否) */ inStrictMode?: boolean; /** * 是否要容忍对 JSExpression 求值时的异常 * 默认:true - * 注: 如果容忍异常,则会在求值时包裹 try-catch 块, + * 注:如果容忍异常,则会在求值时包裹 try-catch 块, * catch 到异常时默认会抛出一个 CustomEvent 事件里面包含异常信息和求值的表达式 */ tolerateEvalErrors?: boolean; /** * 容忍异常的时候的的错误处理语句块 - * 默认: 无 + * 默认:无 * 您可以设置为一个语句块,比如: * window.dispatchEvent(new CustomEvent('lowcode-eval-error', { error, expr })) * * 一般可以结合埋点监控模块用来监控求值异常 * - * 其中: + * 其中: * - error: 异常信息 * - expr: 求值的表达式 */ evalErrorsHandler?: string; + + /** + * Hook which is used to customize original options, we can reorder/add/remove plugins/processors + * of the existing solution. + */ + customizeBuilderOptions?(originalOptions: ProjectBuilderInitOptions): ProjectBuilderInitOptions; } export interface IProjectBuilder { - generateProject: (schema: ProjectSchema | string) => Promise<ResultDir>; + generateProject: (schema: IPublicTypeProjectSchema | string) => Promise<ResultDir>; } /** 项目级别的前置处理器 */ -export type ProjectPreProcessor = (schema: ProjectSchema) => Promise<ProjectSchema> | ProjectSchema; +export type ProjectPreProcessor = (schema: IPublicTypeProjectSchema) => + Promise<IPublicTypeProjectSchema> | IPublicTypeProjectSchema; + +export interface ProjectPostProcessorOptions { + parseResult?: IParseResult; + template?: IProjectTemplate; +} /** 项目级别的后置处理器 */ export type ProjectPostProcessor = ( result: ResultDir, - schema: ProjectSchema, - originalSchema: ProjectSchema | string, + schema: IPublicTypeProjectSchema, + originalSchema: IPublicTypeProjectSchema | string, + options: ProjectPostProcessorOptions, ) => Promise<ResultDir> | ResultDir; /** 模块级别的后置处理器的工厂方法 */ export type PostProcessorFactory<T> = (config?: T) => PostProcessor; /** 模块级别的后置处理器 */ -export type PostProcessor = (content: string, fileType: string) => string; +export type PostProcessor = (content: string, fileType: string, name?: string) => string; // TODO: temp interface, need modify export interface IPluginOptions { @@ -198,20 +215,20 @@ type CompositeTypeGenerator<I, T> = | BaseGenerator<I, T, CompositeValueGeneratorOptions> | Array<BaseGenerator<I, T, CompositeValueGeneratorOptions>>; -export type NodeGenerator<T> = (nodeItem: NodeDataType, scope: IScope) => T; +export type NodeGenerator<T> = (nodeItem: IPublicTypeNodeDataType, scope: IScope) => T; // FIXME: 在新的实现中,添加了第一参数 this: CustomHandlerSet 作为上下文。究其本质 // scopeBindings?: IScopeBindings; -// 这个组合只用来用来处理 CompositeValue 类型,不是这个类型的不要放在这里 +// 这个组合只用来用来处理 IPublicTypeCompositeValue 类型,不是这个类型的不要放在这里 export interface HandlerSet<T> { string?: CompositeTypeGenerator<string, T>; boolean?: CompositeTypeGenerator<boolean, T>; number?: CompositeTypeGenerator<number, T>; - expression?: CompositeTypeGenerator<JSExpression, T>; - function?: CompositeTypeGenerator<JSFunction, T>; - slot?: CompositeTypeGenerator<JSSlot, T>; - array?: CompositeTypeGenerator<JSONArray | CompositeArray, T>; - object?: CompositeTypeGenerator<JSONObject | CompositeObject, T>; + expression?: CompositeTypeGenerator<IPublicTypeJSExpression, T>; + function?: CompositeTypeGenerator<IPublicTypeJSFunction, T>; + slot?: CompositeTypeGenerator<IPublicTypeJSSlot, T>; + array?: CompositeTypeGenerator<IPublicTypeJSONArray | IPublicTypeCompositeArray, T>; + object?: CompositeTypeGenerator<IPublicTypeJSONObject | IPublicTypeCompositeObject, T>; } export interface CompositeValueGeneratorOptions { diff --git a/modules/code-generator/src/types/deps.ts b/modules/code-generator/src/types/deps.ts index cb2fb5eac4..a6531092d5 100644 --- a/modules/code-generator/src/types/deps.ts +++ b/modules/code-generator/src/types/deps.ts @@ -35,4 +35,4 @@ export interface IDependency { main?: string; // 包导出组件入口文件路径 /lib/input dependencyType?: DependencyType; // 依赖类型 内/外 componentName?: string; // 导入后名称 -} +} \ No newline at end of file diff --git a/modules/code-generator/src/types/intermediate.ts b/modules/code-generator/src/types/intermediate.ts index 43d8f0e844..7cba0bd44b 100644 --- a/modules/code-generator/src/types/intermediate.ts +++ b/modules/code-generator/src/types/intermediate.ts @@ -1,4 +1,9 @@ -import { I18nMap, UtilsMap, ContainerSchema, JSONObject } from '@alilc/lowcode-types'; +import { + IPublicTypeI18nMap, + IPublicTypeUtilsMap, + IPublicTypeContainerSchema, + IPublicTypeJSONObject, +} from '@alilc/lowcode-types'; import { IDependency, INpmPackage } from './deps'; import { ICompAnalyzeResult } from './analyze'; @@ -6,7 +11,7 @@ import { ICompAnalyzeResult } from './analyze'; export interface IParseResult { containers: IContainerInfo[]; globalUtils?: IUtilInfo; - globalI18n?: I18nMap; + globalI18n?: IPublicTypeI18nMap; globalRouter?: IRouterInfo; project?: IProjectInfo; } @@ -15,14 +20,14 @@ export interface IWithDependency { deps?: IDependency[]; } -export interface IContainerInfo extends ContainerSchema, IWithDependency { +export interface IContainerInfo extends IPublicTypeContainerSchema, IWithDependency { containerType: string; moduleName: string; analyzeResult?: ICompAnalyzeResult; } export interface IUtilInfo extends IWithDependency { - utils: UtilsMap; + utils: IPublicTypeUtilsMap; } export interface IRouterInfo extends IWithDependency { @@ -33,16 +38,26 @@ export interface IRouterInfo extends IWithDependency { }>; } +/** + * project's remarks + */ +export interface ProjectRemark { + + /** if current project only contain one container which type is `Component` */ + isSingleComponent?: boolean; +} + export interface IProjectInfo { css?: string; containersDeps?: IDependency[]; utilsDeps?: IDependency[]; - constants?: JSONObject; - i18n?: I18nMap; + constants?: IPublicTypeJSONObject; + i18n?: IPublicTypeI18nMap; packages: INpmPackage[]; meta?: { name?: string; title?: string } | Record<string, any>; config?: Record<string, any>; dataSourcesTypes?: string[]; + projectRemark?: ProjectRemark; } export interface IPageMeta { diff --git a/modules/code-generator/src/types/jsx.ts b/modules/code-generator/src/types/jsx.ts index c79f1d2070..28f948d639 100644 --- a/modules/code-generator/src/types/jsx.ts +++ b/modules/code-generator/src/types/jsx.ts @@ -1,4 +1,4 @@ -import { NodeSchema, CompositeValue } from '@alilc/lowcode-types'; +import { IPublicTypeNodeSchema, IPublicTypeCompositeValue } from '@alilc/lowcode-types'; import { HandlerSet, BaseGenerator, NodeGenerator } from './core'; export enum PIECE_TYPE { @@ -17,11 +17,11 @@ export interface CodePiece { export interface AttrData { attrName: string; - attrValue: CompositeValue; + attrValue: IPublicTypeCompositeValue; } -// 对 JSX 出码的理解,目前定制点包含 【包装】【标签名】【属性】 +// 对 JSX 出码的理解,目前定制点包含【包装】【标签名】【属性】 export type AttrPlugin = BaseGenerator<AttrData, CodePiece[], NodeGeneratorConfig>; -export type NodePlugin = BaseGenerator<NodeSchema, CodePiece[], NodeGeneratorConfig>; +export type NodePlugin = BaseGenerator<IPublicTypeNodeSchema, CodePiece[], NodeGeneratorConfig>; export interface NodeGeneratorConfig { handlers?: HandlerSet<string>; @@ -33,7 +33,7 @@ export interface NodeGeneratorConfig { /** * 是否要容忍对 JSExpression 求值时的异常 * 默认:true - * 注: 如果容忍异常,则会在求值时包裹 try-catch 块 -- 通过 __$$eval / __$$evalArray + * 注:如果容忍异常,则会在求值时包裹 try-catch 块 -- 通过 __$$eval / __$$evalArray * catch 到异常时默认会抛出一个 CustomEvent 事件里面包含异常信息和求值的表达式 */ tolerateEvalErrors?: boolean; diff --git a/modules/code-generator/src/utils/common.ts b/modules/code-generator/src/utils/common.ts index cfba393e96..fa7ab30a95 100644 --- a/modules/code-generator/src/utils/common.ts +++ b/modules/code-generator/src/utils/common.ts @@ -1,5 +1,7 @@ +import type { IPublicTypeJSExpression, IPublicTypeJSFunction } from '@alilc/lowcode-types'; import changeCase from 'change-case'; import short from 'short-uuid'; +import { DependencyType, IDependency, IExternalDependency, IInternalDependency } from '../types'; // Doc: https://www.npmjs.com/package/change-case @@ -39,3 +41,19 @@ export function getStaticExprValue<T>(expr: string): T { // eslint-disable-next-line no-new-func return Function(`"use strict";return (${expr})`)(); } + +export function isJSExpressionFn(data: any): data is IPublicTypeJSFunction { + return data?.type === 'JSExpression' && data?.extType === 'function'; +} + +export function isInternalDependency( + dependency: IDependency, +): dependency is IInternalDependency { + return dependency.dependencyType === DependencyType.Internal; +} + +export function isExternalDependency( + dependency: IDependency, +): dependency is IExternalDependency { + return dependency.dependencyType === DependencyType.External; +} \ No newline at end of file diff --git a/modules/code-generator/src/utils/compositeType.ts b/modules/code-generator/src/utils/compositeType.ts index d4885e2243..0dd612641d 100644 --- a/modules/code-generator/src/utils/compositeType.ts +++ b/modules/code-generator/src/utils/compositeType.ts @@ -1,13 +1,13 @@ import { - CompositeArray, - CompositeValue, - CompositeObject, - JSFunction, - JSExpression, + IPublicTypeCompositeArray, + IPublicTypeCompositeValue, + IPublicTypeCompositeObject, + IPublicTypeJSFunction, + IPublicTypeJSExpression, isJSExpression, isJSFunction, isJSSlot, - JSSlot, + IPublicTypeJSSlot, } from '@alilc/lowcode-types'; import _ from 'lodash'; @@ -16,6 +16,7 @@ import { generateExpression, generateFunction } from './jsExpression'; import { generateJsSlot } from './jsSlot'; import { executeFunctionStack } from './aopHelper'; import { parseExpressionGetKeywords } from './expressionParser'; +import { isJSExpressionFn } from './common'; interface ILegaoVariable { type: 'variable'; @@ -43,7 +44,7 @@ function isDataSource(v: unknown): v is DataSource { } function generateArray( - value: CompositeArray, + value: IPublicTypeCompositeArray, scope: IScope, options: CompositeValueGeneratorOptions = {}, ): string { @@ -52,7 +53,7 @@ function generateArray( } function generateObject( - value: CompositeObject, + value: IPublicTypeCompositeObject, scope: IScope, options: CompositeValueGeneratorOptions = {}, ): string { @@ -88,7 +89,7 @@ function generateBool(value: boolean): string { return value ? 'true' : 'false'; } -function genFunction(value: JSFunction): string { +function genFunction(value: IPublicTypeJSFunction): string { const globalVars = parseExpressionGetKeywords(value.value); if (globalVars.includes('arguments')) { @@ -98,7 +99,7 @@ function genFunction(value: JSFunction): string { return generateFunction(value, { isArrow: true }); } -function genJsSlot(value: JSSlot, scope: IScope, options: CompositeValueGeneratorOptions = {}) { +function genJsSlot(value: IPublicTypeJSSlot, scope: IScope, options: CompositeValueGeneratorOptions = {}) { if (options.nodeGenerator) { return generateJsSlot(value, scope, options.nodeGenerator); } @@ -106,7 +107,7 @@ function genJsSlot(value: JSSlot, scope: IScope, options: CompositeValueGenerato } function generateUnknownType( - value: CompositeValue, + value: IPublicTypeCompositeValue, scope: IScope, options: CompositeValueGeneratorOptions = {}, ): string { @@ -128,19 +129,20 @@ function generateUnknownType( // FIXME: 这个是临时方案 // 在遇到 type variable 私有类型时,转换为 JSExpression if (isVariable(value)) { - const transValue: JSExpression = { + const transValue: IPublicTypeJSExpression = { type: 'JSExpression', value: value.variable, }; if (options.handlers?.expression) { - return executeFunctionStack( + const expression = executeFunctionStack( transValue, scope, options.handlers.expression, generateExpression, options, ); + return expression || 'undefined'; } return generateExpression(transValue, scope); } @@ -158,7 +160,7 @@ function generateUnknownType( return generateExpression(value, scope); } - if (isJSFunction(value)) { + if (isJSFunction(value) || isJSExpressionFn(value)) { if (options.handlers?.function) { return executeFunctionStack(value, scope, options.handlers.function, genFunction, options); } @@ -187,7 +189,7 @@ function generateUnknownType( if (options.handlers?.object) { return executeFunctionStack(value, scope, options.handlers.object, generateObject, options); } - return generateObject(value as CompositeObject, scope, options); + return generateObject(value as IPublicTypeCompositeObject, scope, options); } if (_.isString(value)) { @@ -217,7 +219,7 @@ function generateUnknownType( // 这一层曾经是对产出做最外层包装的,但其实包装逻辑不应该属于这一层 // 这一层先不去掉,做冗余,方便后续重构 export function generateCompositeType( - value: CompositeValue, + value: IPublicTypeCompositeValue, scope: IScope, options: CompositeValueGeneratorOptions = {}, ): string { diff --git a/modules/code-generator/src/utils/dataSource.ts b/modules/code-generator/src/utils/dataSource.ts index 610f399344..5595b8defd 100644 --- a/modules/code-generator/src/utils/dataSource.ts +++ b/modules/code-generator/src/utils/dataSource.ts @@ -1,20 +1,24 @@ import changeCase from 'change-case'; import type { IProjectInfo } from '../types/intermediate'; -export type DataSourceDependenciesConfig = { +export interface DataSourceDependenciesConfig { + /** 数据源引擎的版本 */ engineVersion?: string; + /** 数据源引擎的包名 */ enginePackage?: string; + /** 数据源 handlers 的版本 */ handlersVersion?: { [key: string]: string; }; + /** 数据源 handlers 的包名 */ handlersPackages?: { [key: string]: string; }; -}; +} export function buildDataSourceDependencies( ir: IProjectInfo, @@ -22,13 +26,13 @@ export function buildDataSourceDependencies( ): Record<string, string> { return { // 数据源引擎的依赖包 - [cfg.enginePackage || '@alilc/lowcode-datasource-engine']: cfg.engineVersion || 'latest', + [cfg.enginePackage || '@alilc/lowcode-datasource-engine']: cfg.engineVersion || '^1.0.0', // 各种数据源的 handlers 的依赖包 ...(ir.dataSourcesTypes || []).reduce( (acc, dsType) => ({ ...acc, - [getDataSourceHandlerPackageName(dsType)]: cfg.handlersVersion?.[dsType] || 'latest', + [getDataSourceHandlerPackageName(dsType)]: cfg.handlersVersion?.[dsType] || '^1.0.0', }), {}, ), diff --git a/modules/code-generator/src/utils/expressionParser.ts b/modules/code-generator/src/utils/expressionParser.ts index 58c6e6ca8c..15320b9637 100644 --- a/modules/code-generator/src/utils/expressionParser.ts +++ b/modules/code-generator/src/utils/expressionParser.ts @@ -161,9 +161,13 @@ export function parseExpressionGetKeywords(expr: string | null | undefined): str try { const keywordVars = new OrderedSet<string>(); - const ast = parser.parse(`!(${expr});`); + const ast = parser.parse(`!(${expr});`, { + plugins: [ + 'jsx', + ], + }); - const addIdentifierIfNeeded = (x: Record<string, unknown> | number | null | undefined) => { + const addIdentifierIfNeeded = (x: Node | null | undefined) => { if (typeof x === 'object' && isIdentifier(x) && JS_KEYWORDS.includes(x.name)) { keywordVars.add(x.name); } @@ -185,7 +189,7 @@ export function parseExpressionGetKeywords(expr: string | null | undefined): str addIdentifierIfNeeded(item); }); } else { - addIdentifierIfNeeded(fieldValue as Record<string, unknown> | null); + addIdentifierIfNeeded(fieldValue as any); } } }); @@ -213,7 +217,7 @@ export function parseExpressionGetGlobalVariables( const ast = parser.parse(`!(${expr});`); const addUndeclaredIdentifierIfNeeded = ( - x: Record<string, unknown> | number | null | undefined, + x: Node | null | undefined, path: NodePath<Node>, ) => { if (typeof x === 'object' && isIdentifier(x) && !path.scope.hasBinding(x.name)) { @@ -237,7 +241,7 @@ export function parseExpressionGetGlobalVariables( addUndeclaredIdentifierIfNeeded(item, path); }); } else { - addUndeclaredIdentifierIfNeeded(fieldValue as Record<string, unknown> | null, path); + addUndeclaredIdentifierIfNeeded(fieldValue as any, path); } } }); diff --git a/modules/code-generator/src/utils/format.ts b/modules/code-generator/src/utils/format.ts new file mode 100644 index 0000000000..7c865e8f34 --- /dev/null +++ b/modules/code-generator/src/utils/format.ts @@ -0,0 +1,12 @@ +import prettier from 'prettier'; +import parserBabel from 'prettier/parser-babel'; + +export function format(content: string, options = {}) { + return prettier.format(content, { + parser: 'babel', + plugins: [parserBabel], + singleQuote: true, + jsxSingleQuote: false, + ...options, + }); +} diff --git a/modules/code-generator/src/utils/index.ts b/modules/code-generator/src/utils/index.ts index ff5194172d..8c41d678ff 100644 --- a/modules/code-generator/src/utils/index.ts +++ b/modules/code-generator/src/utils/index.ts @@ -11,6 +11,8 @@ import * as schema from './schema'; import * as version from './version'; import * as scope from './Scope'; import * as expressionParser from './expressionParser'; +import * as dataSource from './dataSource'; +import * as pathHelper from './pathHelper'; export { common, @@ -25,4 +27,6 @@ export { version, scope, expressionParser, + dataSource, + pathHelper, }; diff --git a/modules/code-generator/src/utils/jsExpression.ts b/modules/code-generator/src/utils/jsExpression.ts index d9fb67892c..08d2fafd8a 100644 --- a/modules/code-generator/src/utils/jsExpression.ts +++ b/modules/code-generator/src/utils/jsExpression.ts @@ -2,13 +2,19 @@ import * as parser from '@babel/parser'; import generate from '@babel/generator'; import traverse from '@babel/traverse'; import * as t from '@babel/types'; -import { JSExpression, JSFunction, isJSExpression, isJSFunction } from '@alilc/lowcode-types'; +import { IPublicTypeJSExpression, IPublicTypeJSFunction, isJSExpression, isJSFunction } from '@alilc/lowcode-types'; import { CodeGeneratorError, IScope } from '../types'; import { transformExpressionLocalRef, ParseError } from './expressionParser'; +import { isJSExpressionFn } from './common'; function parseFunction(content: string): t.FunctionExpression | null { try { - const ast = parser.parse(`(${content});`); + const ast = parser.parse(`(${content});`, { + plugins: [ + 'jsx', + ], + }); + let resultNode: t.FunctionExpression | null = null; traverse(ast, { FunctionExpression(path) { @@ -73,13 +79,18 @@ function getBodyStatements(content: string) { throw new Error('Can not find Function Statement'); } -export function isJsCode(value: unknown): boolean { - return isJSExpression(value) || isJSFunction(value); +/** + * 是否是广义上的 JSFunction + * @param value + * @returns + */ +export function isBroadJSFunction(value: unknown): boolean { + return isJSExpressionFn(value) || isJSFunction(value); } export function generateExpression(value: any, scope: IScope): string { if (isJSExpression(value)) { - const exprVal = (value as JSExpression).value.trim(); + const exprVal = (value as IPublicTypeJSExpression).value.trim(); if (!exprVal) { return 'null'; } @@ -91,6 +102,10 @@ export function generateExpression(value: any, scope: IScope): string { throw new CodeGeneratorError('Not a JSExpression'); } +function getFunctionSource(cfg: IPublicTypeJSFunction): string { + return cfg.source || cfg.value || cfg.compiled; +} + export function generateFunction( value: any, config: { @@ -107,21 +122,26 @@ export function generateFunction( isBindExpr: false, }, ) { - if (isJsCode(value)) { - const functionCfg = value as JSFunction; + if (isBroadJSFunction(value)) { + const functionCfg = value as IPublicTypeJSFunction; + const functionSource = getFunctionSource(functionCfg); if (config.isMember) { - return transformFuncExpr2MethodMember(config.name || '', functionCfg.value); + return transformFuncExpr2MethodMember(config.name || '', functionSource); } if (config.isBlock) { - return getBodyStatements(functionCfg.value); + return getBodyStatements(functionSource); } if (config.isArrow) { - return getArrowFunction(functionCfg.value); + return getArrowFunction(functionSource); } if (config.isBindExpr) { - return `(${functionCfg.value}).bind(this)`; + return `(${functionSource}).bind(this)`; } - return functionCfg.value; + return functionSource; + } + + if (isJSExpression(value)) { + return value.value; } throw new CodeGeneratorError('Not a JSFunction or JSExpression'); diff --git a/modules/code-generator/src/utils/jsSlot.ts b/modules/code-generator/src/utils/jsSlot.ts index 49f7b31704..265f6857f2 100644 --- a/modules/code-generator/src/utils/jsSlot.ts +++ b/modules/code-generator/src/utils/jsSlot.ts @@ -1,4 +1,4 @@ -import { JSSlot, isJSSlot, NodeData } from '@alilc/lowcode-types'; +import { IPublicTypeJSSlot, isJSSlot, IPublicTypeNodeData } from '@alilc/lowcode-types'; import { CodeGeneratorError, NodeGenerator, IScope } from '../types'; import { unwrapJsExprQuoteInJsx } from './jsxHelpers'; @@ -8,7 +8,7 @@ function generateSingleLineComment(commentText: string): string { export function generateJsSlot(slot: any, scope: IScope, generator: NodeGenerator<string>): string { if (isJSSlot(slot)) { - const { title, params, value } = slot as JSSlot; + const { title, params, value } = slot as IPublicTypeJSSlot; // slot 也是分有参数和无参数的 // - 有参数的 slot 就是类似一个 render 函数,需要创建子作用域 @@ -39,7 +39,7 @@ export function generateJsSlot(slot: any, scope: IScope, generator: NodeGenerato } function generateNodeDataOrArrayForJsSlot( - value: NodeData | NodeData[], + value: IPublicTypeNodeData | IPublicTypeNodeData[], generator: NodeGenerator<string>, scope: IScope, ) { diff --git a/modules/code-generator/src/utils/nodeToJSX.ts b/modules/code-generator/src/utils/nodeToJSX.ts index 0b5919902f..d29f28762c 100644 --- a/modules/code-generator/src/utils/nodeToJSX.ts +++ b/modules/code-generator/src/utils/nodeToJSX.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import { pipe } from 'fp-ts/function'; -import { NodeSchema, isNodeSchema, NodeDataType, CompositeValue } from '@alilc/lowcode-types'; +import { IPublicTypeNodeSchema, isNodeSchema, IPublicTypeNodeDataType, IPublicTypeCompositeValue } from '@alilc/lowcode-types'; import { IScope, @@ -19,6 +19,7 @@ import { executeFunctionStack } from './aopHelper'; import { encodeJsxStringNode } from './encodeJsxAttrString'; import { unwrapJsExprQuoteInJsx } from './jsxHelpers'; import { transformThis2Context } from '../core/jsx/handlers/transformThis2Context'; +import { isValidIdentifier } from './validate'; function mergeNodeGeneratorConfig( cfg1: NodeGeneratorConfig, @@ -57,7 +58,7 @@ export function isPureString(v: string) { } function generateAttrValue( - attrData: { attrName: string; attrValue: CompositeValue }, + attrData: { attrName: string; attrValue: IPublicTypeCompositeValue }, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { @@ -76,7 +77,7 @@ function generateAttrValue( function generateAttr( attrName: string, - attrValue: CompositeValue, + attrValue: IPublicTypeCompositeValue, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { @@ -115,7 +116,7 @@ function generateAttr( } function generateAttrs( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { @@ -126,11 +127,13 @@ function generateAttrs( if (props) { if (!Array.isArray(props)) { Object.keys(props).forEach((propName: string) => { - pieces = pieces.concat(generateAttr(propName, props[propName], scope, config)); + if (isValidIdentifier(propName)) { + pieces = pieces.concat(generateAttr(propName, props[propName] as IPublicTypeCompositeValue, scope, config)); + } }); } else { props.forEach((prop) => { - if (prop.name && !prop.spread) { + if (prop.name && isValidIdentifier(prop.name) && !prop.spread) { pieces = pieces.concat(generateAttr(prop.name, prop.value, scope, config)); } @@ -144,7 +147,7 @@ function generateAttrs( } function generateBasicNode( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { @@ -160,7 +163,7 @@ function generateBasicNode( } function generateSimpleNode( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, ): CodePiece[] { @@ -182,7 +185,7 @@ function generateSimpleNode( function linkPieces(pieces: CodePiece[]): string { const tagsPieces = pieces.filter((p) => p.type === PIECE_TYPE.TAG); if (tagsPieces.length !== 1) { - throw new CodeGeneratorError('One node only need one tag define'); + throw new CodeGeneratorError('Only one tag definition required', tagsPieces); } const tagName = tagsPieces[0].value; @@ -216,13 +219,13 @@ function linkPieces(pieces: CodePiece[]): string { } function generateNodeSchema( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, ): string { const pieces: CodePiece[] = []; if (config?.nodePlugins) { - const res = executeFunctionStack<NodeSchema, CodePiece[], NodeGeneratorConfig>( + const res = executeFunctionStack<IPublicTypeNodeSchema, CodePiece[], NodeGeneratorConfig>( nodeItem, scope, config.nodePlugins, @@ -247,11 +250,11 @@ function generateNodeSchema( * @type NodePlugin Extended * * @export - * @param {NodeSchema} nodeItem 当前 UI 节点 + * @param {IPublicTypeNodeSchema} nodeItem 当前 UI 节点 * @returns {CodePiece[]} 实现功能的相关代码片段 */ export function generateReactLoopCtrl( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, next?: NodePlugin, @@ -270,8 +273,7 @@ export function generateReactLoopCtrl( const loopDataExpr = pipe( nodeItem.loop, // 将 JSExpression 转换为 JS 表达式代码: - (expr) => - generateCompositeType(expr, scope, { + (expr) => generateCompositeType(expr, scope, { handlers: config?.handlers, tolerateEvalErrors: false, // 这个内部不需要包 try catch, 下面会统一加的 }), @@ -302,11 +304,11 @@ export function generateReactLoopCtrl( * @type NodePlugin * * @export - * @param {NodeSchema} nodeItem 当前 UI 节点 + * @param {IPublicTypeNodeSchema} nodeItem 当前 UI 节点 * @returns {CodePiece[]} 实现功能的相关代码片段 */ export function generateConditionReactCtrl( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, next?: NodePlugin, @@ -337,11 +339,11 @@ export function generateConditionReactCtrl( * @type NodePlugin * * @export - * @param {NodeSchema} nodeItem 当前 UI 节点 + * @param {IPublicTypeNodeSchema} nodeItem 当前 UI 节点 * @returns {CodePiece[]} 实现功能的相关代码片段 */ export function generateReactExprInJS( - nodeItem: NodeSchema, + nodeItem: IPublicTypeNodeSchema, scope: IScope, config?: NodeGeneratorConfig, next?: NodePlugin, @@ -366,7 +368,7 @@ export function generateReactExprInJS( const handleChildren = (v: string[]) => v.join(''); export function createNodeGenerator(cfg: NodeGeneratorConfig = {}): NodeGenerator<string> { - const generateNode = (nodeItem: NodeDataType, scope: IScope): string => { + const generateNode = (nodeItem: IPublicTypeNodeDataType, scope: IScope): string => { if (_.isArray(nodeItem)) { const resList = nodeItem.map((n) => generateNode(n, scope)); return handleChildren(resList); @@ -391,8 +393,7 @@ export function createNodeGenerator(cfg: NodeGeneratorConfig = {}): NodeGenerato return `{${valueStr}}`; }; - return (nodeItem: NodeDataType, scope: IScope) => - unwrapJsExprQuoteInJsx(generateNode(nodeItem, scope)); + return (nodeItem: IPublicTypeNodeDataType, scope: IScope) => unwrapJsExprQuoteInJsx(generateNode(nodeItem, scope)); } const defaultReactGeneratorConfig: NodeGeneratorConfig = { diff --git a/modules/code-generator/src/utils/pathHelper.ts b/modules/code-generator/src/utils/pathHelper.ts new file mode 100644 index 0000000000..8440089d89 --- /dev/null +++ b/modules/code-generator/src/utils/pathHelper.ts @@ -0,0 +1,41 @@ +import { IContextData } from '../types'; + +function relativePath(from: string[], to: string[]): string[] { + const length = Math.min(from.length, to.length); + let samePartsLength = length; + for (let i = 0; i < length; i++) { + if (from[i] !== to[i]) { + samePartsLength = i; + break; + } + } + if (samePartsLength === 0) { + return to; + } + let outputParts = []; + for (let i = samePartsLength; i < from.length; i++) { + outputParts.push('..'); + } + outputParts = [...outputParts, ...to.slice(samePartsLength)]; + if (outputParts[0] !== '..') { + outputParts.unshift('.'); + } + return outputParts; +} + +export function getSlotRelativePath(options: { + contextData: IContextData; + from: string; + to: string; +}) { + const { contextData, from, to } = options; + const isSingleComponent = contextData?.extraContextData?.projectRemark?.isSingleComponent; + const template = contextData?.extraContextData?.template; + let toPath = template.slots[to].path; + toPath = [...toPath, template.slots[to].fileName!]; + let fromPath = template.slots[from].path; + if (!isSingleComponent && ['components', 'pages'].indexOf(from) !== -1) { + fromPath = [...fromPath, 'pageName']; + } + return relativePath(fromPath, toPath).join('/'); +} \ No newline at end of file diff --git a/modules/code-generator/src/utils/schema.ts b/modules/code-generator/src/utils/schema.ts index b2faecbc64..f9529945eb 100644 --- a/modules/code-generator/src/utils/schema.ts +++ b/modules/code-generator/src/utils/schema.ts @@ -1,20 +1,21 @@ import * as _ from 'lodash'; import { - JSExpression, - NodeData, - NodeSchema, + IPublicTypeJSExpression, + IPublicTypeNodeData, + IPublicTypeNodeSchema, isJSExpression, isJSSlot, isDOMText, - ContainerSchema, - NpmInfo, - CompositeValue, + IPublicTypeContainerSchema, + IPublicTypeNpmInfo, + IPublicTypeCompositeValue, isNodeSchema, isJSFunction, } from '@alilc/lowcode-types'; import { CodeGeneratorError } from '../types/error'; +import { isJSExpressionFn } from './common'; -export function isContainerSchema(x: any): x is ContainerSchema { +export function isContainerSchema(x: any): x is IPublicTypeContainerSchema { return ( typeof x === 'object' && x && @@ -23,7 +24,7 @@ export function isContainerSchema(x: any): x is ContainerSchema { ); } -export function isNpmInfo(x: any): x is NpmInfo { +export function isNpmInfo(x: any): x is IPublicTypeNpmInfo { return typeof x === 'object' && x && typeof x.package === 'string'; } @@ -43,11 +44,11 @@ const DEFAULT_MAX_DEPTH = 100000; * @returns */ export function handleSubNodes<T>( - children: NodeSchema['children'], + children: IPublicTypeNodeSchema['children'], handlers: { string?: (i: string) => T; - expression?: (i: JSExpression) => T; - node?: (i: NodeSchema) => T; + expression?: (i: IPublicTypeJSExpression) => T; + node?: (i: IPublicTypeNodeSchema) => T; }, options?: { rerun?: boolean; @@ -64,7 +65,7 @@ export function handleSubNodes<T>( } if (Array.isArray(children)) { - const list: NodeData[] = children as NodeData[]; + const list: IPublicTypeNodeData[] = children as IPublicTypeNodeData[]; return list .map((child) => handleSubNodes(child, handlers, { ...opt, maxDepth: maxDepth - 1 })) .reduce((p, c) => p.concat(c), []); @@ -84,7 +85,7 @@ export function handleSubNodes<T>( return handleSubNodes(children.value, handlers, { ...opt, maxDepth: maxDepth - 1 }); } else if (isNodeSchema(children)) { const handler = handlers.node || noop; - const child = children as NodeSchema; + const child = children as IPublicTypeNodeSchema; result = handler(child); if (child.children) { @@ -100,7 +101,7 @@ export function handleSubNodes<T>( }); } else { Object.values(child.props).forEach((value) => { - const childRes = handleCompositeValueInProps(value); + const childRes = handleCompositeValueInProps(value as IPublicTypeCompositeValue); childrenRes.push(...childRes); }); } @@ -115,7 +116,7 @@ export function handleSubNodes<T>( return childrenRes; - function handleCompositeValueInProps(value: CompositeValue): T[] { + function handleCompositeValueInProps(value: IPublicTypeCompositeValue): T[] { if (isJSSlot(value)) { return handleSubNodes(value.value, handlers, { ...opt, maxDepth: maxDepth - 1 }); } @@ -125,9 +126,10 @@ export function handleSubNodes<T>( return _.flatMap(value, (v) => handleCompositeValueInProps(v)); } - // CompositeObject + // IPublicTypeCompositeObject if ( !isJSExpression(value) && + !isJSExpressionFn(value) && !isJSFunction(value) && typeof value === 'object' && value !== null @@ -138,3 +140,17 @@ export function handleSubNodes<T>( return []; } } + +export function isValidContainerType(schema: IPublicTypeNodeSchema) { + return [ + 'Page', + 'Component', + 'Block', + ].includes(schema.componentName); +} + +export const enum ContainerType { + Page = 'Page', + Component = 'Component', + Block = 'Block', +} \ No newline at end of file diff --git a/modules/code-generator/src/utils/theme.ts b/modules/code-generator/src/utils/theme.ts new file mode 100644 index 0000000000..590cf711cf --- /dev/null +++ b/modules/code-generator/src/utils/theme.ts @@ -0,0 +1,20 @@ +/** + * 获取主题信息 + * @param theme theme 形如 @alife/theme-97 或者 @alife/theme-97@^1.0.0 + */ + +export interface ThemeInfo { + name: string; + version?: string; +} + +export function getThemeInfo(theme: string): ThemeInfo { + const sepIdx = theme.indexOf('@', 1); + if (sepIdx === -1) { + return { name: theme }; + } + return { + name: theme.slice(0, sepIdx), + version: theme.slice(sepIdx + 1), + }; +} \ No newline at end of file diff --git a/modules/code-generator/src/utils/validate.ts b/modules/code-generator/src/utils/validate.ts index 4ee0d5b2a3..8d6f9c0d1c 100644 --- a/modules/code-generator/src/utils/validate.ts +++ b/modules/code-generator/src/utils/validate.ts @@ -2,6 +2,10 @@ export const isValidIdentifier = (name: string) => { return /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*$/.test(name); }; +export const isValidComponentName = (name: string) => { + return /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF.]*$/.test(name); +}; + export const ensureValidClassName = (name: string) => { if (!isValidIdentifier(name)) { return `$${name.replace(/[^_$a-zA-Z0-9]/g, '')}`; diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/package.json deleted file mode 100644 index 38cfdd186a..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo01/schema.json5 b/modules/code-generator/test-cases/rax-app/demo01/schema.json5 deleted file mode 100644 index 282f979e89..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo01/schema.json5 +++ /dev/null @@ -1,55 +0,0 @@ -{ - // 本例是一个非常简单的 Hello world 页面 - // Schema 参见:https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr - version: '1.0.0', - componentsMap: [ - { - componentName: 'Page', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'Page', - }, - { - componentName: 'Text', - package: 'rax-text', - version: '^1.0.0', - destructuring: false, - exportName: 'Text', - }, - ], - componentsTree: [ - { - componentName: 'Page', - props: {}, - lifeCycles: {}, - fileName: 'home', - meta: { - router: '/', - }, - dataSource: { - list: [], - }, - children: [ - { - componentName: 'Text', - props: {}, - children: 'Hello world!', - }, - ], - }, - ], - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'root', - }, - meta: { - name: 'Rax App Demo', - git_group: 'demo-group', - project_name: 'demo-project', - description: '这是一个示例应用', - spma: 'spmademo', - creator: '张三', - }, -} diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/package.json deleted file mode 100644 index ca5a0f59cb..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-url-params-handler": "latest", - "@alilc/lowcode-datasource-fetch-handler": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0", - "rax-image": "^1.0.0", - "moment": "*", - "lodash": "*", - "universal-toast": "^1.2.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx deleted file mode 100644 index 7bfda59caf..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx +++ /dev/null @@ -1,349 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:rax 框架的导出名和各种组件名除外。 -import { createElement, Component } from 'rax'; -import { getSearchParams as __$$getSearchParams } from 'rax-app'; - -import Page from 'rax-view'; - -import View from 'rax-view'; - -import Text from 'rax-text'; - -import Image from 'rax-image'; - -import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; - -import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; - -import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; - -import { isMiniApp as __$$isMiniApp } from 'universal-env'; - -import __$$constants from '../../constants'; - -import * as __$$i18n from '../../i18n'; - -import __$$projectUtils from '../../utils'; - -import './index.css'; - -class Home$$Page extends Component { - state = { - clickCount: 0, - user: { - name: '张三', - age: 18, - avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png', - }, - orders: [ - { - title: '【小米智能生活】米家扫地机器人家用全自动扫拖一体机拖地吸尘器', - price: 1799, - coverUrl: 'https://gw.alicdn.com/tfs/TB1dGVlRfb2gK0jSZK9XXaEgFXa-258-130.png', - }, - { - title: '【限时下单立减】Apple/苹果 iPhone 11 4G全网通智能手机正品苏宁易购官方旗舰店苹果11', - price: 4999, - coverUrl: 'https://gw.alicdn.com/tfs/TB18gdJddTfau8jSZFwXXX1mVXa-1298-1202.png', - }, - ], - }; - - _methods = this._defineMethods(); - - _context = this._createContext(); - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { - runtimeConfig: true, - requestHandlersMap: { - urlParams: __$$createUrlParamsRequestHandler(__$$getSearchParams()), - fetch: __$$createFetchRequestHandler(), - }, - }); - - _utils = this._defineUtils(); - - _lifeCycles = this._defineLifeCycles(); - - constructor(props, context) { - super(props); - - __$$i18n._inject2(this); - } /* end of constructor */ - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - - this._lifeCycles.didMount(); - } /* end of componentDidMount */ - - componentWillUnmount() { - this._lifeCycles.willUnmount(); - } /* end of componentWillUnmount */ - - render() { - const __$$context = this._context; - const { - state, - setState, - dataSourceMap, - reloadDataSource, - utils, - constants, - i18n, - i18nFormat, - getLocale, - setLocale, - } = __$$context; - - return ( - <Page> - <View> - <Text>Demo data source logic</Text> - </View> - <View> - <Text>=== User Info: ===</Text> - </View> - {!!__$$eval(() => __$$context.state.user) && ( - <View style={{ flexDirection: 'row' }}> - <Image - source={{ uri: __$$eval(() => __$$context.state.user.avatar) }} - style={{ width: '32px', height: '32px' }} - /> - <View onClick={__$$context.hello}> - <Text>{__$$eval(() => __$$context.state.user.name)}</Text> - <Text>{__$$eval(() => __$$context.state.user.age)}岁</Text> - </View> - </View> - )} - <View> - <Text>=== Orders: ===</Text> - </View> - {__$$evalArray(() => __$$eval(() => __$$context.state.orders)).map((order, index) => - ((__$$context) => ( - <View - style={{ flexDirection: 'row' }} - data-order={order} - onClick={(...__$$args) => { - if (__$$isMiniApp) { - const __$$event = __$$args[0]; - const order = __$$event.target.dataset.order; - return function () { - __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); - }.apply(this, __$$args); - } else { - return function () { - __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); - }.apply(this, __$$args); - } - }} - > - <View> - <Image source={{ uri: __$$eval(() => order.coverUrl) }} style={{ width: '80px', height: '60px' }} /> - </View> - <View> - <Text>{__$$eval(() => order.title)}</Text> - <Text>{__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))}</Text> - </View> - </View> - ))(__$$createChildContext(__$$context, { order, index })), - )} - <View - onClick={function () { - __$$context.setState({ - clickCount: __$$context.state.clickCount + 1, - }); - }} - > - <Text>点击次数:{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text> - </View> - <View> - <Text>操作提示:</Text> - <Text>1. 点击会员名,可以弹出 Toast "Hello xxx!"</Text> - <Text>2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示</Text> - <Text>3. 最下面的【点击次数】,点一次应该加 1</Text> - </View> - </Page> - ); - } /* end of render */ - - _createContext() { - const self = this; - const context = { - get state() { - return self.state; - }, - setState(newState, callback) { - self.setState(newState, callback); - }, - get dataSourceMap() { - return self._dataSourceEngine.dataSourceMap || {}; - }, - async reloadDataSource() { - await self._dataSourceEngine.reloadDataSource(); - }, - get utils() { - return self._utils; - }, - get page() { - return context; - }, - get component() { - return context; - }, - get props() { - return self.props; - }, - get constants() { - return __$$constants; - }, - i18n: __$$i18n.i18n, - i18nFormat: __$$i18n.i18nFormat, - getLocale: __$$i18n.getLocale, - setLocale(locale) { - __$$i18n.setLocale(locale); - self.forceUpdate(); - }, - ...this._methods, - }; - - return context; - } - - _defineDataSourceConfig() { - const __$$context = this._context; - return { - list: [ - { - errorHandler: function (err) { - setTimeout(() => { - __$$context.setState({ - __refresh: Date.now() + Math.random(), - }); - }, 0); - throw err; - }, - id: 'urlParams', - type: 'urlParams', - isInit: true, - options: function () { - return undefined; - }, - }, - { - errorHandler: function (err) { - setTimeout(() => { - __$$context.setState({ - __refresh: Date.now() + Math.random(), - }); - }, 0); - throw err; - }, - id: 'user', - type: 'fetch', - options: function () { - return { - method: 'GET', - uri: 'https://shs.xxx.com/mock/1458/demo/user', - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.success) { - throw new Error(response.message); - } - - return response.data; - }, - isInit: true, - }, - { - errorHandler: function (err) { - setTimeout(() => { - __$$context.setState({ - __refresh: Date.now() + Math.random(), - }); - }, 0); - throw err; - }, - id: 'orders', - type: 'fetch', - options: function () { - return { - method: 'GET', - uri: __$$context.state.user.ordersApiUri, - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.success) { - throw new Error(response.message); - } - - return response.data.result; - }, - isInit: true, - }, - ], - dataHandler: function (dataMap) { - console.info('All datasources loaded:', dataMap); - }, - }; - } - - _defineUtils() { - return { - ...__$$projectUtils, - }; - } - - _defineLifeCycles() { - const __$$lifeCycles = { - didMount: function didMount() { - this.utils.Toast.show(`Hello ${this.state.user.name}!`); - }, - - willUnmount: function didMount() { - this.utils.Toast.show(`Bye, ${this.state.user.name}!`); - }, - }; - - // 为所有的方法绑定上下文 - Object.entries(__$$lifeCycles).forEach(([lifeCycleName, lifeCycleMethod]) => { - if (typeof lifeCycleMethod === 'function') { - __$$lifeCycles[lifeCycleName] = (...args) => { - return lifeCycleMethod.apply(this._context, args); - }; - } - }); - - return __$$lifeCycles; - } - - _defineMethods() { - return { - hello: function hello() { - this.utils.Toast.show(`Hello ${this.state.user.name}!`); - console.log(`Hello ${this.state.user.name}!`); - }, - }; - } -} - -export default Home$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - return Object.assign({}, oldContext, ext); -} diff --git a/modules/code-generator/test-cases/rax-app/demo02/schema.json5 b/modules/code-generator/test-cases/rax-app/demo02/schema.json5 deleted file mode 100644 index fa39cdb45d..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo02/schema.json5 +++ /dev/null @@ -1,375 +0,0 @@ -{ - // 本例是一个比较复杂的,带有循环和条件渲染的,以及有各种事件处理函数的页面 - // Schema 参见:https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr - version: '1.0.0', - componentsMap: [ - { - componentName: 'View', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'View', - }, - { - componentName: 'Text', - package: 'rax-text', - version: '^1.0.0', - destructuring: false, - exportName: 'Text', - }, - { - componentName: 'Image', - package: 'rax-image', - version: '^1.0.0', - destructuring: false, - exportName: 'Image', - }, - { - componentName: 'Page', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'Page', - }, - ], - componentsTree: [ - { - componentName: 'Page', - fileName: 'home', - meta: { - router: '/', - }, - state: { - clickCount: 0, - user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' }, - orders: [ - { - title: '【小米智能生活】米家扫地机器人家用全自动扫拖一体机拖地吸尘器', - price: 1799, - coverUrl: 'https://gw.alicdn.com/tfs/TB1dGVlRfb2gK0jSZK9XXaEgFXa-258-130.png', - }, - { - title: '【限时下单立减】Apple/苹果 iPhone 11 4G全网通智能手机正品苏宁易购官方旗舰店苹果11', - price: 4999, - coverUrl: 'https://gw.alicdn.com/tfs/TB18gdJddTfau8jSZFwXXX1mVXa-1298-1202.png', - }, - ], - }, - props: {}, - lifeCycles: { - didMount: { - type: 'JSExpression', - value: 'function didMount(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n}', - }, - willUnmount: { - type: 'JSExpression', - value: 'function didMount(){\n this.utils.Toast.show(`Bye, ${this.state.user.name}!`);\n}', - }, - }, - methods: { - hello: { - type: 'JSExpression', - value: 'function hello(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n console.log(`Hello ${this.state.user.name}!`); }', - }, - }, - dataSource: { - list: [ - { - id: 'urlParams', - type: 'urlParams', - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/user - { - id: 'user', - type: 'fetch', - options: { - method: 'GET', - uri: 'https://shs.xxx.com/mock/1458/demo/user', - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data;\n}', - }, - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders - { - id: 'orders', - type: 'fetch', - options: { - method: 'GET', - uri: { - type: 'JSExpression', - value: 'this.state.user.ordersApiUri', - }, - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data.result;\n}', - }, - }, - ], - dataHandler: { - type: 'JSExpression', - value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', - }, - }, - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: {}, - children: 'Demo data source logic', - }, - ], - }, - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: {}, - children: '=== User Info: ===', - }, - ], - }, - { - componentName: 'View', - condition: { - type: 'JSExpression', - value: 'this.state.user', - }, - props: { - style: { flexDirection: 'row' }, - }, - children: [ - { - componentName: 'Image', - props: { - source: { - uri: { - type: 'JSExpression', - value: 'this.state.user.avatar', - }, - }, - style: { - width: '32px', - height: '32px', - }, - }, - }, - { - componentName: 'View', - props: { - onClick: { - type: 'JSExpression', - value: 'this.hello', - }, - }, - children: [ - { - componentName: 'Text', - children: { - type: 'JSExpression', - value: 'this.state.user.name', - }, - }, - { - componentName: 'Text', - children: [ - { - type: 'JSExpression', - value: 'this.state.user.age', - }, - '岁', - ], - }, - ], - }, - ], - }, - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: {}, - children: '=== Orders: ===', - }, - ], - }, - { - componentName: 'View', - loop: { - type: 'JSExpression', - value: 'this.state.orders', - }, - loopArgs: ['order', 'index'], - props: { - style: { flexDirection: 'row' }, - onClick: { - type: 'JSExpression', - value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }', - }, - }, - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Image', - props: { - source: { - uri: { - type: 'JSExpression', - value: 'this.order.coverUrl', - }, - }, - style: { - width: '80px', - height: '60px', - }, - }, - }, - ], - }, - { - componentName: 'View', - children: [ - { - componentName: 'Text', - children: { - type: 'JSExpression', - value: 'this.order.title', - }, - }, - { - componentName: 'Text', - children: { - type: 'JSExpression', - value: 'this.utils.formatPrice(this.order.price, "元")', - }, - }, - ], - }, - ], - }, - { - componentName: 'View', - props: { - onClick: { - type: 'JSExpression', - value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }', - }, - }, - children: [ - { - componentName: 'Text', - children: [ - '点击次数:', - { - type: 'JSExpression', - value: 'this.state.clickCount', - }, - '(点击加 1)', - ], - }, - ], - }, - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: {}, - children: '操作提示:', - }, - { - componentName: 'Text', - props: {}, - children: '1. 点击会员名,可以弹出 Toast "Hello xxx!"', - }, - { - componentName: 'Text', - props: {}, - children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示', - }, - { - componentName: 'Text', - props: {}, - children: '3. 最下面的【点击次数】,点一次应该加 1', - }, - ], - }, - ], - }, - ], - utils: [ - // 可以直接定义一个函数 - { - name: 'formatPrice', - type: 'function', - content: { - type: 'JSExpression', - value: 'function formatPrice(price, unit) { return Number(price).toFixed(2) + unit; }', - }, - }, - // 在 utils 里面也可以用 this 访问当前上下文: - { - name: 'recordEvent', - type: 'function', - content: { - type: 'JSExpression', - value: 'function recordEvent(eventName, eventDetail) { \n this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);\n console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user); }', - }, - }, - // 也可以直接从 npm 包引入 (下例等价于 `import moment from 'moment';`) - { - name: 'moment', - type: 'npm', - content: { - package: 'moment', - version: '*', - exportName: 'moment', - }, - }, - // 可以引入子目录(下例等价于 `import clone from 'lodash/clone';`) - { - name: 'clone', - type: 'npm', - content: { - package: 'lodash', - version: '*', - exportName: 'clone', - destructuring: false, - main: '/clone', - }, - }, - { - name: 'Toast', - type: 'npm', - content: { - package: 'universal-toast', - version: '^1.2.0', - exportName: 'Toast', // TODO: 这个 exportName 是否可以省略?省略后默认是上一层的 name? - }, - }, - ], - css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}', - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'root', - }, - meta: { - name: 'Rax App Demo', - git_group: 'demo-group', - project_name: 'demo-project', - description: '这是一个示例应用', - spma: 'spmademo', - creator: '张三', - }, -} diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/package.json deleted file mode 100644 index 48690ff4d9..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0", - "rax-link": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo03/schema.json5 b/modules/code-generator/test-cases/rax-app/demo03/schema.json5 deleted file mode 100644 index 5652b1db1a..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo03/schema.json5 +++ /dev/null @@ -1,176 +0,0 @@ -{ - // 本例是一个路由测试页面,里面有几个页面,相互之间有跳转关系的 - // Schema 参见:https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr - version: '1.0.0', - componentsMap: [ - { - componentName: 'View', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'View', - }, - { - componentName: 'Text', - package: 'rax-text', - version: '^1.0.0', - destructuring: false, - exportName: 'Text', - }, - { - componentName: 'Link', - package: 'rax-link', - version: '^1.0.0', - destructuring: false, - exportName: 'Link', - }, - { - componentName: 'Image', - package: 'rax-image', - version: '^1.0.0', - destructuring: false, - exportName: 'Image', - }, - { - componentName: 'Page', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'Page', - }, - ], - componentsTree: [ - { - componentName: 'Page', - fileName: 'home', - state: {}, - dataSource: { - list: [], - }, - meta: { - router: '/', - }, - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - children: 'This is the Home Page', - }, - ], - }, - { - componentName: 'Link', - props: { - href: '#/list', - miniappHref: 'navigate:/pages/List/index', - }, - children: [ - { - componentName: 'Text', - children: 'Go To The List Page', - }, - ], - }, - ], - }, - { - componentName: 'Page', - fileName: 'list', - state: {}, - dataSource: { - list: [], - }, - meta: { - router: '/list', - }, - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - children: 'This is the List Page', - }, - ], - }, - { - componentName: 'Link', - props: { - href: '#/detail', - miniappHref: 'navigate:/pages/Detail/index', - }, - children: [ - { - componentName: 'Text', - children: 'Go To The Detail Page', - }, - ], - }, - { - componentName: 'Link', - props: { - href: 'javascript:history.back();', - miniappHref: 'navigateBack:', - }, - children: [ - { - componentName: 'Text', - children: 'Go back', - }, - ], - }, - ], - }, - { - componentName: 'Page', - fileName: 'detail', - state: {}, - dataSource: { - list: [], - }, - meta: { - router: '/detail', - }, - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - children: 'This is the Detail Page', - }, - ], - }, - { - componentName: 'Link', - props: { - href: 'javascript:history.back();', - miniappHref: 'navigateBack:', - }, - children: [ - { - componentName: 'Text', - children: 'Go back', - }, - ], - }, - ], - }, - ], - css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}', - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'root', - }, - meta: { - name: 'Rax App Demo', - git_group: 'demo-group', - project_name: 'demo-project', - description: '这是一个示例应用', - spma: 'spmademo', - creator: '张三', - }, -} diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/package.json deleted file mode 100644 index 16fa70bbc1..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "@alife/right-design-card": "*", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/package.json deleted file mode 100644 index 38cfdd186a..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo05/schema.json5 b/modules/code-generator/test-cases/rax-app/demo05/schema.json5 deleted file mode 100644 index 3c767dfd9b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo05/schema.json5 +++ /dev/null @@ -1,73 +0,0 @@ -{ - // 这是一个关于国际化的 schema 示例 - // Schema 参见:https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr - version: '1.0.0', - componentsMap: [ - { - componentName: 'Page', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'Page', - }, - { - componentName: 'Text', - package: 'rax-text', - version: '^1.0.0', - destructuring: false, - exportName: 'Text', - }, - ], - componentsTree: [ - { - componentName: 'Page', - props: {}, - lifeCycles: {}, - fileName: 'home', - meta: { - router: '/', - }, - dataSource: { - list: [], - }, - children: [ - { - componentName: 'Text', - props: { - onClick: { - type: 'JSFunction', - value: "function () {\n this.setLocale(this.getLocale() === 'en-US' ? 'zh-CN' : 'en-US');\n}", - }, - }, - children: [ - { - type: 'JSExpression', - value: 'this.i18n["hello-world"]', - }, - ], - }, - ], - }, - ], - i18n: { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, - }, - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'root', - }, - meta: { - name: 'Rax App Demo', - git_group: 'demo-group', - project_name: 'demo-project', - description: '这是一个示例应用', - spma: 'spmademo', - creator: '张三', - }, -} diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json deleted file mode 100644 index bf31a967ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-table": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json deleted file mode 100644 index 38cfdd186a..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json deleted file mode 100644 index bf31a967ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-table": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json deleted file mode 100644 index bf31a967ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-table": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json deleted file mode 100644 index bf31a967ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-table": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json deleted file mode 100644 index 60f0cb38a1..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-url-params-handler": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "@alilc/b6-page": "^0.1.0", - "@alilc/b6-text": "^0.1.0", - "@alilc/b6-compact-legao-builtin": "1.x", - "antd": "3.x", - "@alilc/b6-util-mocks": "1.x" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/package.json deleted file mode 100644 index 38cfdd186a..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "rax-view": "^1.0.0", - "rax-text": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js deleted file mode 100644 index acf6cd388b..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,75 +0,0 @@ -const i18nConfig = { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, -}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/schema.json5 b/modules/code-generator/test-cases/rax-app/demo12-refs/schema.json5 deleted file mode 100644 index 4f5f5db7f2..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo12-refs/schema.json5 +++ /dev/null @@ -1,74 +0,0 @@ -{ - // 这是一个关于国际化的 schema 示例 - // Schema 参见:https://yuque.antfin-inc.com/mo/spec/spec-materials#eNCJr - version: '1.0.0', - componentsMap: [ - { - componentName: 'Page', - package: 'rax-view', - version: '^1.0.0', - destructuring: false, - exportName: 'Page', - }, - { - componentName: 'Text', - package: 'rax-text', - version: '^1.0.0', - destructuring: false, - exportName: 'Text', - }, - ], - componentsTree: [ - { - componentName: 'Page', - props: {}, - lifeCycles: {}, - fileName: 'home', - meta: { - router: '/', - }, - dataSource: { - list: [], - }, - children: [ - { - componentName: 'Text', - props: { - ref: 'helloText', - onClick: { - type: 'JSFunction', - value: "function () {\n this.setLocale(this.getLocale() === 'en-US' ? 'zh-CN' : 'en-US');\n}", - }, - }, - children: [ - { - type: 'JSExpression', - value: 'this.i18n["hello-world"]', - }, - ], - }, - ], - }, - ], - i18n: { - 'zh-CN': { - 'hello-world': '你好,世界!', - }, - 'en-US': { - 'hello-world': 'Hello world!', - }, - }, - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'root', - }, - meta: { - name: 'Rax App Demo', - git_group: 'demo-group', - project_name: 'demo-project', - description: '这是一个示例应用', - spma: 'spmademo', - creator: '张三', - }, -} diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json b/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json deleted file mode 100644 index 3e59d29844..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rax-demo-app", - "private": true, - "version": "1.0.0", - "scripts": { - "start": "rax-app start", - "build": "rax-app build", - "eslint": "eslint --ext .js,.jsx ./", - "stylelint": "stylelint \"**/*.{css,scss,less}\"", - "prettier": "prettier **/* --write", - "lint": "npm run eslint && npm run stylelint" - }, - "dependencies": { - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-http-handler": "latest", - "universal-env": "^3.2.0", - "intl-messageformat": "^9.3.6", - "rax": "^1.1.0", - "rax-document": "^0.1.6", - "@alilc/lowcode-components": "^1.0.0" - }, - "devDependencies": { - "@iceworks/spec": "^1.0.0", - "rax-app": "^3.0.0", - "eslint": "^6.8.0", - "prettier": "^2.1.2", - "stylelint": "^13.7.2" - } -} diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js deleted file mode 100644 index 2c28c064ea..0000000000 --- a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,68 +0,0 @@ -const i18nConfig = {}; - -let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === 'string' ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; - if (msg == null) { - console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || 'zh_CN'] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/package.json deleted file mode 100644 index 767ec3898f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-url-params-handler": "latest", - "@alilc/lowcode-datasource-fetch-handler": "latest", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index 2f53ec4558..0000000000 --- a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,191 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Form, Input, NumberPicker, Select, Button } from "@alifd/next"; - -import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from "@alilc/lowcode-datasource-url-params-handler"; - -import { createFetchHandler as __$$createFetchRequestHandler } from "@alilc/lowcode-datasource-fetch-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Test$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { - urlParams: __$$createUrlParamsRequestHandler(window.location.search), - fetch: __$$createFetchRequestHandler(), - }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { text: "outter" }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - id: "urlParams", - type: "urlParams", - isInit: function () { - return undefined; - }, - options: function () { - return undefined; - }, - }, - { - id: "user", - type: "fetch", - options: function () { - return { - method: "GET", - uri: "https://shs.xxx.com/mock/1458/demo/user", - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.data.success) { - throw new Error(response.data.message); - } - - return response.data.data; - }, - isInit: function () { - return undefined; - }, - }, - { - id: "orders", - type: "fetch", - options: function () { - return { - method: "GET", - uri: "https://shs.xxx.com/mock/1458/demo/orders", - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.data.success) { - throw new Error(response.data.message); - } - - return response.data.data.result; - }, - isInit: function () { - return undefined; - }, - }, - ], - dataHandler: function (dataMap) { - console.info("All datasources loaded:", dataMap); - }, - }; - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - - console.log("componentDidMount"); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div ref={this._refsManager.linkRef("outterView")} autoLoading={true}> - <Form - labelCol={__$$eval(() => this.state.colNum)} - style={{}} - ref={this._refsManager.linkRef("testForm")} - > - <Form.Item label="姓名:" name="name" initValue="李雷"> - <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> - </Form.Item> - <Form.Item label="年龄:" name="age" initValue="22"> - <NumberPicker size="medium" type="normal" /> - </Form.Item> - <Form.Item label="职业:" name="profession"> - <Select - dataSource={[ - { label: "教师", value: "t" }, - { label: "医生", value: "d" }, - { label: "歌手", value: "s" }, - ]} - /> - </Form.Item> - <div style={{ textAlign: "center" }}> - <Button.Group> - {__$$evalArray(() => ["a", "b", "c"]).map((item, index) => - ((__$$context) => - !!__$$eval(() => index >= 1) && ( - <Button type="primary" style={{ margin: "0 5px 0 5px" }}> - {__$$eval(() => item)} - </Button> - ))(__$$createChildContext(__$$context, { item, index })) - )} - </Button.Group> - </div> - </Form> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo1/schema.json5 b/modules/code-generator/test-cases/react-app/demo1/schema.json5 deleted file mode 100644 index 5a1cfc4d9a..0000000000 --- a/modules/code-generator/test-cases/react-app/demo1/schema.json5 +++ /dev/null @@ -1,276 +0,0 @@ -{ - "version": "1.0.0", - "componentsMap": [ - { - "componentName": "Button", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Button" - }, - { - "componentName": "Button.Group", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Button", - "subName": "Group" - }, - { - "componentName": "Input", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Input" - }, - { - "componentName": "Form", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Form" - }, - { - "componentName": "Form.Item", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Form", - "subName": "Item" - }, - { - "componentName": "NumberPicker", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "NumberPicker" - }, - { - "componentName": "Select", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Select" - } - ], - "componentsTree": [ - { - "componentName": "Page", - "id": "node$1", - "meta": { - "title": "测试", - "router": "/" - }, - "props": { - "ref": "outterView", - "autoLoading": true - }, - "fileName": "test", - "state": { - "text": "outter" - }, - "lifeCycles": { - "componentDidMount": { - "type": "JSExpression", - "value": "function() { console.log('componentDidMount'); }" - } - }, - dataSource: { - list: [ - { - id: 'urlParams', - type: 'urlParams', - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/user - { - id: 'user', - type: 'fetch', - options: { - method: 'GET', - uri: 'https://shs.xxx.com/mock/1458/demo/user', - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', - }, - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders - { - id: 'orders', - type: 'fetch', - options: { - method: 'GET', - uri: "https://shs.xxx.com/mock/1458/demo/orders", - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', - }, - }, - ], - dataHandler: { - type: 'JSExpression', - value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', - }, - }, - "children": [ - { - "componentName": "Form", - "id": "node$2", - "props": { - "labelCol": { - "type": "JSExpression", - "value": "this.state.colNum" - }, - "style": {}, - "ref": "testForm" - }, - "children": [ - { - "componentName": "Form.Item", - "id": "node$3", - "props": { - "label": "姓名:", - "name": "name", - "initValue": "李雷" - }, - "children": [ - { - "componentName": "Input", - "id": "node$4", - "props": { - "placeholder": "请输入", - "size": "medium", - "style": { - "width": 320 - } - } - } - ] - }, - { - "componentName": "Form.Item", - "id": "node$5", - "props": { - "label": "年龄:", - "name": "age", - "initValue": "22" - }, - "children": [ - { - "componentName": "NumberPicker", - "id": "node$6", - "props": { - "size": "medium", - "type": "normal" - } - } - ] - }, - { - "componentName": "Form.Item", - "id": "node$7", - "props": { - "label": "职业:", - "name": "profession" - }, - "children": [ - { - "componentName": "Select", - "id": "node$8", - "props": { - "dataSource": [ - { - "label": "教师", - "value": "t" - }, - { - "label": "医生", - "value": "d" - }, - { - "label": "歌手", - "value": "s" - } - ] - } - } - ] - }, - { - "componentName": "Div", - "id": "node$9", - "props": { - "style": { - "textAlign": "center" - } - }, - "children": [ - { - "componentName": "Button.Group", - "id": "node$a", - "props": {}, - "children": [ - { - "componentName": "Button", - "id": "node$b", - "condition": { - "type": "JSExpression", - "value": "this.index >= 1" - }, - "loop": ["a", "b", "c"], - "props": { - "type": "primary", - "style": { - "margin": "0 5px 0 5px" - }, - }, - "children": [ - { - "type": "JSExpression", - "value": "this.item" - } - ] - } - ] - } - ] - } - ] - } - ] - } - ], - "constants": { - "ENV": "prod", - "DOMAIN": "xxx.xxx.com" - }, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "config": { - "sdkVersion": "1.0.3", - "historyMode": "hash", - "targetRootID": "J_Container", - "layout": { - "componentName": "BasicLayout", - "props": { - "logo": "...", - "name": "测试网站" - } - }, - "theme": { - "package": "@alife/theme-fusion", - "version": "^0.1.0", - "primary": "#ff9966" - } - }, - "meta": { - "name": "demo应用", - "git_group": "appGroup", - "project_name": "app_demo", - "description": "这是一个测试应用", - "spma": "spa23d", - "creator": "月飞" - } -} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json deleted file mode 100644 index df2e306244..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-url-params-handler": "latest", - "@alilc/b6-page": "^0.1.0", - "@alilc/b6-text": "^0.1.0", - "@alilc/b6-compact-legao-builtin": "1.x", - "antd": "3.x", - "@alilc/b6-util-mocks": "1.x" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx deleted file mode 100644 index a4751b6f7f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx +++ /dev/null @@ -1,112 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Page } from "@alilc/b6-page"; - -import { Text } from "@alilc/b6-text"; - -import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from "@alilc/lowcode-datasource-url-params-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Aaaa$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { - urlParams: __$$createUrlParamsRequestHandler(window.location.search), - }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - __$$i18n._inject2(this); - - this.state = {}; - } - - $ = () => null; - - $$ = () => []; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - id: "urlParams", - type: "urlParams", - description: "URL参数", - options: function () { - return { - uri: "", - }; - }, - isInit: function () { - return undefined; - }, - }, - ], - }; - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div title="" backgroundColor="#fff" textColor="#333" style={{}}> - <Text - content="欢迎使用 BuildSuccess!sadsad" - style={{}} - fieldId="text_kp6ci11t" - /> - </div> - ); - } -} - -export default Aaaa$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js deleted file mode 100644 index 00dbd626f4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Aaaa from "@/pages/Aaaa"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "/aaaa", - component: Aaaa, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js deleted file mode 100644 index 8b08776e3f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js +++ /dev/null @@ -1,61 +0,0 @@ -import legaoBuiltin from "@alilc/b6-compact-legao-builtin"; - -import { message, Modal as modal } from "antd"; - -import { mocks } from "@alilc/b6-util-mocks"; - -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default { - legaoBuiltin, - - message, - - mocks, - - modal, -}; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/package.json deleted file mode 100644 index b3eeeae4e2..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/constants.js b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/constants.js deleted file mode 100644 index c4a5859ee4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -const __$$constants = { ENV: "prod", DOMAIN: "xxx.xxx.com" }; - -export default __$$constants; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/global.scss deleted file mode 100644 index 2d97c56b09..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/global.scss +++ /dev/null @@ -1,13 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} - -body { - font-size: 12px; -} -.table { - width: 100px; -} diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/i18n.js deleted file mode 100644 index 3f0a822cee..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,86 +0,0 @@ -const i18nConfig = { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国", - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China", - }, -}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index eda8c2fbe5..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,113 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Form, Input, NumberPicker, Select, Button } from "@alifd/next"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { text: "outter" }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - componentDidMount() { - console.log("componentDidMount"); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div ref={this._refsManager.linkRef("outterView")} autoLoading={true}> - <Form - labelCol={__$$eval(() => this.state.colNum)} - style={{}} - ref={this._refsManager.linkRef("testForm")} - > - <Form.Item - label={__$$eval(() => this.i18n("i18n-jwg27yo4"))} - name="name" - initValue="李雷" - > - <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> - </Form.Item> - <Form.Item label="年龄:" name="age" initValue="22"> - <NumberPicker size="medium" type="normal" /> - </Form.Item> - <Form.Item label="职业:" name="profession"> - <Select - dataSource={[ - { label: "教师", value: "t" }, - { label: "医生", value: "d" }, - { label: "歌手", value: "s" }, - ]} - /> - </Form.Item> - <div style={{ textAlign: "center" }}> - <Button.Group> - <Button - type="primary" - style={{ margin: "0 5px 0 5px" }} - htmlType="submit" - > - 提交 - </Button> - <Button - type="normal" - style={{ margin: "0 5px 0 5px" }} - htmlType="reset" - > - 重置 - </Button> - </Button.Group> - </div> - </Form> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/routes.js deleted file mode 100644 index e6b7426d47..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "/", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo2/schema.json5 b/modules/code-generator/test-cases/react-app/demo2/schema.json5 deleted file mode 100644 index 8678b4e925..0000000000 --- a/modules/code-generator/test-cases/react-app/demo2/schema.json5 +++ /dev/null @@ -1,256 +0,0 @@ -{ - "version": "1.0.0", - "componentsMap": [ - { - "componentName": "Button", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Button" - }, - { - "componentName": "Button.Group", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Button", - "subName": "Group" - }, - { - "componentName": "Input", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Input" - }, - { - "componentName": "Form", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Form" - }, - { - "componentName": "Form.Item", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Form", - "subName": "Item" - }, - { - "componentName": "NumberPicker", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "NumberPicker" - }, - { - "componentName": "Select", - "package": "@alifd/next", - "version": "1.19.18", - "destructuring": true, - "exportName": "Select" - } - ], - "componentsTree": [ - { - "componentName": "Page", - "id": "node$1", - "meta": { - "title": "测试", - "router": "/" - }, - "props": { - "ref": "outterView", - "autoLoading": true - }, - "fileName": "test", - "state": { - "text": "outter" - }, - "lifeCycles": { - "componentDidMount": { - "type": "JSExpression", - "value": "function() { console.log('componentDidMount'); }" - } - }, - "children": [ - { - "componentName": "Form", - "id": "node$2", - "props": { - "labelCol": { - "type": "JSExpression", - "value": "this.state.colNum" - }, - "style": {}, - "ref": "testForm" - }, - "children": [ - { - "componentName": "Form.Item", - "id": "node$3", - "props": { - "label": { - type: 'JSExpression', - value: 'this.i18n("i18n-jwg27yo4")', - }, - "name": "name", - "initValue": "李雷" - }, - "children": [ - { - "componentName": "Input", - "id": "node$4", - "props": { - "placeholder": "请输入", - "size": "medium", - "style": { - "width": 320 - } - } - } - ] - }, - { - "componentName": "Form.Item", - "id": "node$5", - "props": { - "label": "年龄:", - "name": "age", - "initValue": "22" - }, - "children": [ - { - "componentName": "NumberPicker", - "id": "node$6", - "props": { - "size": "medium", - "type": "normal" - } - } - ] - }, - { - "componentName": "Form.Item", - "id": "node$7", - "props": { - "label": "职业:", - "name": "profession" - }, - "children": [ - { - "componentName": "Select", - "id": "node$8", - "props": { - "dataSource": [ - { - "label": "教师", - "value": "t" - }, - { - "label": "医生", - "value": "d" - }, - { - "label": "歌手", - "value": "s" - } - ] - } - } - ] - }, - { - "componentName": "Div", - "id": "node$9", - "props": { - "style": { - "textAlign": "center" - } - }, - "children": [ - { - "componentName": "Button.Group", - "id": "node$a", - "props": {}, - "children": [ - { - "componentName": "Button", - "id": "node$b", - "props": { - "type": "primary", - "style": { - "margin": "0 5px 0 5px" - }, - "htmlType": "submit" - }, - "children": [ - "提交" - ] - }, - { - "componentName": "Button", - "id": "node$d", - "props": { - "type": "normal", - "style": { - "margin": "0 5px 0 5px" - }, - "htmlType": "reset" - }, - "children": [ - "重置" - ] - } - ] - } - ] - } - ] - } - ] - } - ], - "constants": { - "ENV": "prod", - "DOMAIN": "xxx.xxx.com" - }, - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China" - } - }, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "config": { - "sdkVersion": "1.0.3", - "historyMode": "hash", - "targetRootID": "J_Container", - "layout": { - "componentName": "BasicLayout", - "props": { - "logo": "...", - "name": "测试网站" - } - }, - "theme": { - "package": "@alife/theme-fusion", - "version": "^0.1.0", - "primary": "#ff9966" - } - }, - "meta": { - "name": "demo应用", - "git_group": "appGroup", - "project_name": "app_demo", - "description": "这是一个测试应用", - "spma": "spa23d", - "creator": "月飞" - } -} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/package.json deleted file mode 100644 index b3eeeae4e2..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/constants.js b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/constants.js deleted file mode 100644 index c4a5859ee4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -const __$$constants = { ENV: "prod", DOMAIN: "xxx.xxx.com" }; - -export default __$$constants; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/global.scss deleted file mode 100644 index 2d97c56b09..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/global.scss +++ /dev/null @@ -1,13 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} - -body { - font-size: 12px; -} -.table { - width: 100px; -} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/i18n.js deleted file mode 100644 index 3f0a822cee..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,86 +0,0 @@ -const i18nConfig = { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国", - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China", - }, -}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index de9b20393c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,87 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import Super, { - Button, - Input as CustomInput, - Form, - NumberPicker, - Select, - SearchTable as SearchTableExport, -} from "@alifd/next"; - -import SuperOther from "@alifd/next"; - -import utils from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const SuperSub = Super.Sub; - -const SelectOption = Select.Option; - -const SearchTable = SearchTableExport.default; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - __$$i18n._inject2(this); - - this.state = {}; - } - - $ = () => null; - - $$ = () => []; - - componentDidMount() {} - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div> - <Super title={__$$eval(() => this.state.title)} /> - <SuperSub /> - <SuperOther /> - <Button /> - <Button.Group /> - <CustomInput /> - <Form.Item /> - <NumberPicker /> - <SelectOption /> - <SearchTable /> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/routes.js deleted file mode 100644 index e6b7426d47..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "/", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/package.json deleted file mode 100644 index 51a11a8a9b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-fetch-handler": "latest", - "@alife/container": "latest", - "@alife/mc-assets-1935": "0.1.9" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index b81e72ad79..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,286 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { - Page as NextPage, - Block as NextBlock, - P as NextP, - Text as NextText, -} from "@alife/container/lib/index.js"; - -import { AliSearchTable as AliSearchTableExport } from "@alife/mc-assets-1935/build/lowcode/index.js"; - -import { createFetchHandler as __$$createFetchRequestHandler } from "@alilc/lowcode-datasource-fetch-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const NextBlockCell = NextBlock.Cell; - -const AliSearchTable = AliSearchTableExport.default; - -class Test$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { fetch: __$$createFetchRequestHandler() }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { text: "outter", isShowDialog: false }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - type: "fetch", - isInit: function () { - return true; - }, - options: function () { - return { - params: {}, - method: "GET", - isCors: true, - timeout: 5000, - headers: {}, - uri: "https://mocks.xxx.com/mock/jjpin/user/list", - }; - }, - id: "users", - }, - ], - }; - } - - componentWillUnmount() { - console.log("will umount"); - } - - componentDidUpdate(prevProps, prevState, snapshot) { - console.log(this.state); - } - - testFunc() { - console.log("test func"); - } - - onClick() { - this.setState({ - isShowDialog: true, - }); - } - - closeDialog() { - this.setState({ - isShowDialog: false, - }); - } - - onSearch(values) { - console.log("search form:", values); - console.log(this.dataSourceMap); - this.dataSourceMap["users"].load(values); - } - - onClear() { - console.log("form reset"); - this.setState({ - isShowDialog: true, - }); - } - - onPageChange(page, pageSize) { - console.log(`page: ${page}, pageSize: ${pageSize}`); - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - - console.log("did mount"); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div - ref={this._refsManager.linkRef("outterView")} - style={{ height: "100%" }} - > - <NextPage - columns={12} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={ - <NextP - wrap={true} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - <NextText type="h5">员工列表</NextText> - </NextP> - } - headerTest={[]} - headerProps={{ background: "surface" }} - footer={null} - minHeight="100vh" - > - <NextBlock - prefix="next-" - placeholderStyle={{ height: "100%" }} - noPadding={false} - noBorder={false} - background="surface" - colSpan={12} - rowSpan={1} - childTotalColumns="1fr" - > - <NextBlockCell - title="" - primaryKey="732" - prefix="next-" - placeholderStyle={{ height: "100%" }} - colSpan={1} - rowSpan={1} - > - <NextP - wrap={true} - type="body2" - textSpacing={true} - verAlign="center" - align="flex-start" - flex={true} - > - <AliSearchTable - dataSource={__$$eval(() => this.state.users.data)} - rowKey="workid" - columns={[ - { title: "花名", dataIndex: "cname" }, - { title: "user_id", dataIndex: "workid" }, - { title: "部门", dataIndex: "dep" }, - ]} - searchItems={[ - { label: "姓名", name: "cname" }, - { label: "部门", name: "dep" }, - ]} - onSearch={function () { - return this.onSearch.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - onClear={function () { - return this.onClear.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - pagination={{ - defaultPageSize: "", - onPageChange: function () { - return this.onPageChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - showSizeChanger: true, - }} - /> - </NextP> - </NextBlockCell> - </NextBlock> - </NextPage> - <NextPage - columns={12} - headerDivider={true} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={null} - headerProps={{ background: "surface" }} - footer={null} - minHeight="100vh" - > - <NextBlock - prefix="next-" - placeholderStyle={{ height: "100%" }} - noPadding={false} - noBorder={false} - background="surface" - colSpan={12} - rowSpan={1} - childTotalColumns={1} - > - <NextBlockCell - title="" - primaryKey="472" - prefix="next-" - placeholderStyle={{ height: "100%" }} - colSpan={1} - rowSpan={1} - /> - </NextBlock> - </NextPage> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/routes.js deleted file mode 100644 index ce50d58b70..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo4/schema.json5 b/modules/code-generator/test-cases/react-app/demo4/schema.json5 deleted file mode 100644 index 0695b843ee..0000000000 --- a/modules/code-generator/test-cases/react-app/demo4/schema.json5 +++ /dev/null @@ -1,353 +0,0 @@ -{ - "version": "1.0.0", - "componentsMap": [ - { - "package": "@alife/mc-assets-1935", - "version": "0.1.9", - "exportName": "AliSearchTable", - "main": "build/lowcode/index.js", - "subName": "default", - "destructuring": true, - "componentName": "AliSearchTable" - }, - { - "package": "@alife/container", - "version": "latest", - "exportName": "P", - "main": "lib/index.js", - "destructuring": true, - "subName": "", - "componentName": "NextP" - }, - { - "package": "@alife/container", - "version": "latest", - "exportName": "Block", - "main": "lib/index.js", - "destructuring": true, - "subName": "Cell", - "componentName": "NextBlockCell" - }, - { - "package": "@alife/container", - "version": "latest", - "exportName": "Block", - "main": "lib/index.js", - "destructuring": true, - "subName": "", - "componentName": "NextBlock" - }, - { - "package": "@alife/container", - "version": "latest", - "exportName": "Text", - "main": "lib/index.js", - "destructuring": true, - "subName": "", - "componentName": "NextText" - }, - { - "package": "@alife/container", - "version": "latest", - "exportName": "Page", - "main": "lib/index.js", - "destructuring": true, - "subName": "", - "componentName": "NextPage" - } - ], - "componentsTree": [ - { - "componentName": "Page", - "id": "node_dockcviv8fo1", - "props": { - "ref": "outterView", - "style": { - "height": "100%" - } - }, - "fileName": "test", - "dataSource": { - "list": [ - { - "type": "fetch", - "isInit": true, - "options": { - "params": {}, - "method": "GET", - "isCors": true, - "timeout": 5000, - "headers": {}, - "uri": "https://mocks.xxx.com/mock/jjpin/user/list" - }, - "id": "users" - } - ] - }, - "css": "body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}", - "lifeCycles": { - "componentDidMount": { - "type": "JSFunction", - "value": "function() {\n console.log('did mount');\n }" - }, - "componentWillUnmount": { - "type": "JSFunction", - "value": "function() {\n console.log('will umount');\n }" - }, - "componentDidUpdate": { - "type": "JSFunction", - "value": "function(prevProps, prevState, snapshot) {\n console.log(this.state);\n }" - } - }, - "methods": { - "testFunc": { - "type": "JSFunction", - "value": "function() {\n console.log('test func');\n }" - }, - "onClick": { - "type": "JSFunction", - "value": "function() {\n this.setState({\n isShowDialog: true\n })\n }" - }, - "closeDialog": { - "type": "JSFunction", - "value": "function() {\n this.setState({\n isShowDialog: false\n })\n }" - }, - "onSearch": { - "type": "JSFunction", - "value": "function(values) {\n console.log('search form:', values)\n console.log(this.dataSourceMap);\n this.dataSourceMap['users'].load(values)\n }" - }, - "onClear": { - "type": "JSFunction", - "value": "function() {\n console.log('form reset')\n this.setState({\n isShowDialog: true\n })\n }" - }, - "onPageChange": { - "type": "JSFunction", - "value": "function(page, pageSize) {\n console.log(`page: ${page}, pageSize: ${pageSize}`)\n }" - } - }, - "state": { - "text": "outter", - "isShowDialog": false - }, - "children": [ - { - "componentName": "NextPage", - "id": "node_ockkgjwi8z1", - "props": { - "columns": 12, - "placeholderStyle": { - "gridRowEnd": "span 1", - "gridColumnEnd": "span 12" - }, - "placeholder": "页面主体内容:拖拽Block布局组件到这里", - "header": { - "type": "JSSlot", - "value": [ - { - "componentName": "NextP", - "id": "node_ockkgjwi8zn", - "props": { - "wrap": true, - "type": "body2", - "verAlign": "middle", - "textSpacing": true, - "align": "left", - "flex": true - }, - "children": [ - { - "componentName": "NextText", - "id": "node_ockkgjwi8zo", - "props": { - "type": "h5", - "children": "员工列表" - } - } - ] - } - ], - "title": "header" - }, - "headerTest": { - "type": "JSSlot", - "value": [], - "title": "header" - }, - "headerProps": { - "background": "surface" - }, - "footer": { - "type": "JSSlot", - "title": "footer" - }, - "minHeight": "100vh" - }, - "children": [ - { - "componentName": "NextBlock", - "id": "node_ockkgjwi8z2", - "props": { - "prefix": "next-", - "placeholderStyle": { - "height": "100%" - }, - "noPadding": false, - "noBorder": false, - "background": "surface", - "colSpan": 12, - "rowSpan": 1, - "childTotalColumns": "1fr" - }, - "title": "分区", - "children": [ - { - "componentName": "NextBlockCell", - "id": "node_ockkgjwi8z3", - "props": { - "title": "", - "primaryKey": "732", - "prefix": "next-", - "placeholderStyle": { - "height": "100%" - }, - "colSpan": 1, - "rowSpan": 1 - }, - "children": [ - { - "componentName": "NextP", - "id": "node_ockkgjwi8zu", - "props": { - "wrap": true, - "type": "body2", - "textSpacing": true, - "verAlign": "center", - "align": "flex-start", - "flex": true - }, - "children": [ - { - "componentName": "AliSearchTable", - "id": "node_ockkgjwi8zv", - "props": { - "dataSource": { - "type": "JSExpression", - "value": "this.state.users.data" - }, - "rowKey": "workid", - "columns": [ - { - "title": "花名", - "dataIndex": "cname" - }, - { - "title": "user_id", - "dataIndex": "workid" - }, - { - "title": "部门", - "dataIndex": "dep" - } - ], - "searchItems": [ - { - "label": "姓名", - "name": "cname" - }, - { - "label": "部门", - "name": "dep" - } - ], - "onSearch": { - "type": "JSFunction", - "value": "function(){ return this.onSearch.apply(this,Array.prototype.slice.call(arguments).concat([])) }" - }, - "onClear": { - "type": "JSFunction", - "value": "function(){ return this.onClear.apply(this,Array.prototype.slice.call(arguments).concat([])) }" - }, - "pagination": { - "defaultPageSize": "", - "onPageChange": { - "type": "JSFunction", - "value": "function(){ return this.onPageChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }" - }, - "showSizeChanger": true - } - } - } - ] - } - ] - } - ] - } - ] - }, - { - "componentName": "NextPage", - "id": "node_ockm4jxd6313", - "props": { - "columns": 12, - "headerDivider": true, - "placeholderStyle": { - "gridRowEnd": "span 1", - "gridColumnEnd": "span 12" - }, - "placeholder": "页面主体内容:拖拽Block布局组件到这里", - "header": { - "type": "JSSlot", - "title": "header" - }, - "headerProps": { - "background": "surface" - }, - "footer": { - "type": "JSSlot", - "title": "footer" - }, - "minHeight": "100vh" - }, - "title": "页面", - "children": [ - { - "componentName": "NextBlock", - "id": "node_ockm4jxd6314", - "props": { - "prefix": "next-", - "placeholderStyle": { - "height": "100%" - }, - "noPadding": false, - "noBorder": false, - "background": "surface", - "colSpan": 12, - "rowSpan": 1, - "childTotalColumns": 1 - }, - "title": "区块", - "children": [ - { - "componentName": "NextBlockCell", - "id": "node_ockm4jxd6315", - "props": { - "title": "", - "primaryKey": "472", - "prefix": "next-", - "placeholderStyle": { - "height": "100%" - }, - "colSpan": 1, - "rowSpan": 1 - } - } - ] - } - ] - } - ] - } - ], - "i18n": {} -} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/package.json deleted file mode 100644 index 747fc287cf..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "undefined": "*", - "@alife/container": "0.3.7", - "@alilc/antd-lowcode": "0.5.4", - "@alife/mc-assets-1935": "0.1.16" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index 430f8aad97..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,383 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { - Page as NextPage, - Block as NextBlock, - P as NextP, -} from "@alife/container/lib/index.js"; - -import { - Card, - Space, - Typography, - Select, - Button, - Modal, - Form, - InputNumber, - Input, -} from "@alilc/antd-lowcode/dist/antd-lowcode.esm.js"; - -import { AliAutoSearchTable } from "@alife/mc-assets-1935/build/lowcode/index.js"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const NextBlockCell = NextBlock.Cell; - -const AliAutoSearchTableDefault = AliAutoSearchTable.default; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { - name: "nongzhou", - gateways: [], - selectedGateway: null, - records: [], - modalVisible: false, - }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - componentWillUnmount() { - /* ... */ - } - - componentDidUpdate() { - /* ... */ - } - - onChange() { - /* ... */ - } - - getActions() { - /* ... */ - } - - onCreateOrder() { - /* ... */ - } - - onCancelModal() { - /* ... */ - } - - onConfirmCreateOrder() { - /* ... */ - } - - componentDidMount() {} - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div - ref={this._refsManager.linkRef("outterView")} - style={{ height: "100%" }} - > - <NextPage - columns={12} - headerDivider={true} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={null} - headerProps={{ background: "surface" }} - footer={null} - minHeight="100vh" - style={{ cursor: "pointer" }} - > - <NextBlock - prefix="next-" - placeholderStyle={{ height: "100%" }} - noPadding={false} - noBorder={false} - background="surface" - layoutmode="O" - colSpan={12} - rowSpan={1} - childTotalColumns={12} - > - <NextBlockCell - title="" - prefix="next-" - placeholderStyle={{ height: "100%" }} - layoutmode="O" - childTotalColumns={12} - isAutoContainer={true} - colSpan={12} - rowSpan={1} - > - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - full={true} - flex={true} - > - <Card title=""> - <Space size={0} align="center" direction="horizontal"> - <Typography.Text>所在网关:</Typography.Text> - <Select - style={{ - marginTop: "16px", - marginRight: "16px", - marginBottom: "16px", - marginLeft: "16px", - width: "400px", - display: "inline-block", - }} - options={__$$eval(() => this.state.gateways)} - mode="single" - defaultValue={["auto-edd-uniproxy"]} - labelInValue={true} - showSearch={true} - allowClear={false} - placeholder="请选取网关" - showArrow={true} - loading={false} - tokenSeparators={[]} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onChange", - relatedEventName: "onChange", - }, - ], - eventList: [ - { name: "onBlur", disabled: false }, - { name: "onChange", disabled: true }, - { name: "onDeselect", disabled: false }, - { name: "onFocus", disabled: false }, - { name: "onInputKeyDown", disabled: false }, - { name: "onMouseEnter", disabled: false }, - { name: "onMouseLeave", disabled: false }, - { name: "onPopupScroll", disabled: false }, - { name: "onSearch", disabled: false }, - { name: "onSelect", disabled: false }, - { name: "onDropdownVisibleChange", disabled: false }, - ], - }} - onChange={function () { - this.onChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - /> - </Space> - <Button - type="primary" - style={{ - display: "block", - marginTop: "20px", - marginBottom: "20px", - }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onCreateOrder", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onCreateOrder.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 创建发布单 - </Button> - <Modal - title="创建发布单" - visible={__$$eval(() => this.state.modalVisible)} - footer="" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onCancel", - relatedEventName: "onCancelModal", - }, - ], - eventList: [ - { name: "onCancel", disabled: true }, - { name: "onOk", disabled: false }, - ], - }} - onCancel={function () { - this.onCancelModal.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - zIndex={2000} - > - <Form - labelCol={{ span: 6 }} - wrapperCol={{ span: 14 }} - onFinish={function () { - this.onConfirmCreateOrder.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinish", - relatedEventName: "onConfirmCreateOrder", - }, - ], - eventList: [ - { name: "onFinish", disabled: true }, - { name: "onFinishFailed", disabled: false }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: false }, - ], - }} - > - <Form.Item label="发布批次"> - <InputNumber value={3} min={1} /> - </Form.Item> - <Form.Item label="批次间隔时间"> - <InputNumber value={3} /> - </Form.Item> - <Form.Item label="备注 "> - <Input.TextArea rows={3} placeholder="请输入" /> - </Form.Item> - <Form.Item - wrapperCol={{ offset: 6 }} - style={{ - flexDirection: "row", - alignItems: "flex-end", - justifyContent: "center", - display: "flex", - }} - labelAlign="right" - > - <Button type="primary" htmlType="submit"> - 提交 - </Button> - <Button - style={{ marginLeft: 20 }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onCancelModal", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onCancelModal.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 取消 - </Button> - </Form.Item> - </Form> - </Modal> - <AliAutoSearchTableDefault - rowKey="key" - dataSource={__$$eval(() => this.state.records)} - columns={[ - { - title: "发布名称", - dataIndex: "order_name", - key: "name", - }, - { - title: "类型", - dataIndex: "order_type_desc", - key: "age", - }, - { - title: "发布状态", - dataIndex: "order_status_desc", - key: "address", - }, - { title: "发布人", dataIndex: "creator_name" }, - { title: "当前批次/总批次", dataIndex: "cur_batch_no" }, - { - title: "发布机器/总机器", - dataIndex: "pubblish_ip_finish_num", - }, - { title: "发布时间", dataIndex: "publish_id" }, - ]} - actions={__$$eval(() => this.actions || [])} - getActions={function () { - return this.getActions.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - /> - </Card> - </NextP> - </NextBlockCell> - </NextBlock> - </NextPage> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/routes.js deleted file mode 100644 index ce50d58b70..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json deleted file mode 100644 index 767ec3898f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-url-params-handler": "latest", - "@alilc/lowcode-datasource-fetch-handler": "latest", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js deleted file mode 100644 index c4a5859ee4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -const __$$constants = { ENV: "prod", DOMAIN: "xxx.xxx.com" }; - -export default __$$constants; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss deleted file mode 100644 index 2d97c56b09..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss +++ /dev/null @@ -1,13 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} - -body { - font-size: 12px; -} -.table { - width: 100px; -} diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index d3b1bd37e6..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,191 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Form, Input, NumberPicker, Select, Button } from "@alifd/next"; - -import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from "@alilc/lowcode-datasource-url-params-handler"; - -import { createFetchHandler as __$$createFetchRequestHandler } from "@alilc/lowcode-datasource-fetch-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Test$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { - urlParams: __$$createUrlParamsRequestHandler(window.location.search), - fetch: __$$createFetchRequestHandler(), - }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { text: "outter" }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - id: "urlParams", - type: "urlParams", - isInit: function () { - return undefined; - }, - options: function () { - return undefined; - }, - }, - { - id: "user", - type: "fetch", - options: function () { - return { - method: "GET", - uri: "https://shs.xxx.com/mock/1458/demo/user", - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.data.success) { - throw new Error(response.data.message); - } - - return response.data.data; - }, - isInit: function () { - return undefined; - }, - }, - { - id: "orders", - type: "fetch", - options: function () { - return { - method: "GET", - uri: "https://shs.xxx.com/mock/1458/demo/orders", - isSync: true, - }; - }, - dataHandler: function (response) { - if (!response.data.success) { - throw new Error(response.data.message); - } - - return response.data.data.result; - }, - isInit: function () { - return undefined; - }, - }, - ], - dataHandler: function (dataMap) { - console.info("All datasources loaded:", dataMap); - }, - }; - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - - console.log("componentDidMount"); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div ref={this._refsManager.linkRef("outterView")} autoLoading={true}> - <Form - labelCol={__$$eval(() => this.state.colNum)} - style={{}} - ref={this._refsManager.linkRef("testForm")} - > - <Form.Item label="姓名:" name="name" initValue="李雷"> - <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> - </Form.Item> - <Form.Item label="年龄:" name="age" initValue="22"> - <NumberPicker size="medium" type="normal" /> - </Form.Item> - <Form.Item label="职业:" name="profession"> - <Select - dataSource={[ - { label: "教师", value: "t" }, - { label: "医生", value: "d" }, - { label: "歌手", value: "s" }, - ]} - /> - </Form.Item> - <div style={{ textAlign: "center" }}> - <Button.Group> - {__$$evalArray(() => ["a", "b", "c"]).map((item, index) => - ((__$$context) => - !!false && ( - <Button type="primary" style={{ margin: "0 5px 0 5px" }}> - {__$$eval(() => item)} - </Button> - ))(__$$createChildContext(__$$context, { item, index })) - )} - </Button.Group> - </div> - </Form> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js deleted file mode 100644 index e6b7426d47..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "/", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/schema.json5 b/modules/code-generator/test-cases/react-app/demo6-literal-condition/schema.json5 deleted file mode 100644 index 6d3a601373..0000000000 --- a/modules/code-generator/test-cases/react-app/demo6-literal-condition/schema.json5 +++ /dev/null @@ -1,273 +0,0 @@ -{ - version: '1.0.0', - componentsMap: [ - { - componentName: 'Button', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Button', - }, - { - componentName: 'Button.Group', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Button', - subName: 'Group', - }, - { - componentName: 'Input', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Input', - }, - { - componentName: 'Form', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Form', - }, - { - componentName: 'Form.Item', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Form', - subName: 'Item', - }, - { - componentName: 'NumberPicker', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'NumberPicker', - }, - { - componentName: 'Select', - package: '@alifd/next', - version: '1.19.18', - destructuring: true, - exportName: 'Select', - }, - ], - componentsTree: [ - { - componentName: 'Page', - id: 'node$1', - meta: { - title: '测试', - router: '/', - }, - props: { - ref: 'outterView', - autoLoading: true, - }, - fileName: 'test', - state: { - text: 'outter', - }, - lifeCycles: { - componentDidMount: { - type: 'JSExpression', - value: "function() { console.log('componentDidMount'); }", - }, - }, - dataSource: { - list: [ - { - id: 'urlParams', - type: 'urlParams', - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/user - { - id: 'user', - type: 'fetch', - options: { - method: 'GET', - uri: 'https://shs.xxx.com/mock/1458/demo/user', - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', - }, - }, - // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders - { - id: 'orders', - type: 'fetch', - options: { - method: 'GET', - uri: 'https://shs.xxx.com/mock/1458/demo/orders', - isSync: true, - }, - dataHandler: { - type: 'JSExpression', - value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', - }, - }, - ], - dataHandler: { - type: 'JSExpression', - value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', - }, - }, - children: [ - { - componentName: 'Form', - id: 'node$2', - props: { - labelCol: { - type: 'JSExpression', - value: 'this.state.colNum', - }, - style: {}, - ref: 'testForm', - }, - children: [ - { - componentName: 'Form.Item', - id: 'node$3', - props: { - label: '姓名:', - name: 'name', - initValue: '李雷', - }, - children: [ - { - componentName: 'Input', - id: 'node$4', - props: { - placeholder: '请输入', - size: 'medium', - style: { - width: 320, - }, - }, - }, - ], - }, - { - componentName: 'Form.Item', - id: 'node$5', - props: { - label: '年龄:', - name: 'age', - initValue: '22', - }, - children: [ - { - componentName: 'NumberPicker', - id: 'node$6', - props: { - size: 'medium', - type: 'normal', - }, - }, - ], - }, - { - componentName: 'Form.Item', - id: 'node$7', - props: { - label: '职业:', - name: 'profession', - }, - children: [ - { - componentName: 'Select', - id: 'node$8', - props: { - dataSource: [ - { - label: '教师', - value: 't', - }, - { - label: '医生', - value: 'd', - }, - { - label: '歌手', - value: 's', - }, - ], - }, - }, - ], - }, - { - componentName: 'Div', - id: 'node$9', - props: { - style: { - textAlign: 'center', - }, - }, - children: [ - { - componentName: 'Button.Group', - id: 'node$a', - props: {}, - children: [ - { - componentName: 'Button', - id: 'node$b', - condition: false, - loop: ['a', 'b', 'c'], - props: { - type: 'primary', - style: { - margin: '0 5px 0 5px', - }, - }, - children: [ - { - type: 'JSExpression', - value: 'this.item', - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - }, - ], - constants: { - ENV: 'prod', - DOMAIN: 'xxx.xxx.com', - }, - css: 'body {font-size: 12px;} .table { width: 100px;}', - config: { - sdkVersion: '1.0.3', - historyMode: 'hash', - targetRootID: 'J_Container', - layout: { - componentName: 'BasicLayout', - props: { - logo: '...', - name: '测试网站', - }, - }, - theme: { - package: '@alife/theme-fusion', - version: '^0.1.0', - primary: '#ff9966', - }, - }, - meta: { - name: 'demo应用', - git_group: 'appGroup', - project_name: 'app_demo', - description: '这是一个测试应用', - spma: 'spa23d', - creator: '月飞', - }, -} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json deleted file mode 100644 index bb42d509d6..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "undefined": "*", - "@alilc/antd-lowcode": "0.8.0", - "@alife/container": "0.3.7" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index 79fe13a04b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,1065 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { - Modal, - Steps, - Form, - Input, - Checkbox, - Select, - DatePicker, - InputNumber, - Button, -} from "@alilc/antd-lowcode/dist/antd-lowcode.esm.js"; - -import { - Text as NextText, - Page as NextPage, - Block as NextBlock, - P as NextP, -} from "@alife/container/lib/index.js"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const NextBlockCell = NextBlock.Cell; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { - books: [], - currentStep: 0, - isModifyDialogVisible: false, - isModifyStatus: false, - secondCommitText: "完成并提交", - thirdAuditText: "审核中", - thirdButtonText: "修改", - customerProjectInfo: { - id: null, - systemProjectName: null, - projectVersionTypeArray: null, - projectVersionType: null, - versionLine: 2, - expectedTime: null, - expectedNum: null, - projectModal: null, - displayWidth: null, - displayHeight: null, - displayInch: null, - displayDpi: null, - mainSoc: null, - cpuCoreNum: null, - instructions: null, - osVersion: null, - status: null, - }, - versionLinesArray: [ - { label: "AmapAuto 485", value: 1 }, - { label: "AmapAuto 505", value: 2 }, - ], - projectModalsArray: [ - { label: "车机", value: 1 }, - { label: "车镜", value: 2 }, - { label: "记录仪", value: 3 }, - { label: "其他", value: 4 }, - ], - osVersionsArray: [ - { label: "安卓5", value: 1 }, - { label: "安卓6", value: 2 }, - { label: "安卓7", value: 3 }, - { label: "安卓8", value: 4 }, - { label: "安卓9", value: 5 }, - { label: "安卓10", value: 6 }, - ], - instructionsArray: [ - { label: "ARM64-V8", value: "ARM64-V8" }, - { label: "ARM32-V7", value: "ARM32-V7" }, - { label: "X86", value: "X86" }, - { label: "X64", value: "X64" }, - ], - }; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - componentDidUpdate(prevProps, prevState, snapshot) {} - - componentWillUnmount() {} - - __jp__init() { - /*...*/ - } - - __jp__initRouter() { - /*...*/ - } - - __jp__initDataSource() { - /*...*/ - } - - __jp__initEnv() { - /*...*/ - } - - __jp__initUtils() { - /*...*/ - } - - onFinishFirst() { - /*...*/ - } - - onClickPreSecond() { - /*...*/ - } - - onFinishSecond() { - /*...*/ - } - - onClickModifyThird() { - /*...*/ - } - - onOkModifyDialogThird() { - //第三步 修改 对话框 确定 - this.setState({ - currentStep: 0, - isModifyDialogVisible: false, - }); - } - - onCancelModifyDialogThird() { - //第三步 修改 对话框 取消 - this.setState({ - isModifyDialogVisible: false, - }); - } - - onFinishFailed() {} - - onClickPreThird() { - // 第三步 上一步 - this.setState({ - currentStep: 1, - }); - } - - onClickFirstBack() { - // 第一步 返回按钮 - this.$router.push("/myProjectList"); - } - - onClickSecondBack() { - // 第二步 返回按钮 - this.$router.push("/myProjectList"); - } - - onClickThirdBack() { - // 第三步 返回按钮 - this.$router.push("/myProjectList"); - } - - onValuesChange(_, values) { - this.setState({ - customerProjectInfo: { ...this.state.customerProjectInfo, ...values }, - }); - } - - componentDidMount() {} - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div - ref={this._refsManager.linkRef("outterView")} - style={{ height: "100%" }} - > - <Modal - title="是否修改" - visible={__$$eval(() => this.state.isModifyDialogVisible)} - okText="确认" - okType="" - forceRender={false} - cancelText="取消" - zIndex={2000} - destroyOnClose={false} - confirmLoading={false} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onOk", - relatedEventName: "onOkModifyDialogThird", - }, - { - type: "componentEvent", - name: "onCancel", - relatedEventName: "onCancelModifyDialogThird", - }, - ], - eventList: [ - { name: "onCancel", disabled: true }, - { name: "onOk", disabled: true }, - ], - }} - onOk={function () { - this.onOkModifyDialogThird.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - onCancel={function () { - this.onCancelModifyDialogThird.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - <NextText - type="inherit" - style={{ - fontStyle: "normal", - textAlign: "left", - display: "block", - fontFamily: "arial, helvetica, microsoft yahei", - fontWeight: "normal", - }} - > - 修改将撤回此前填写的信息 - </NextText> - </Modal> - <NextPage - columns={12} - headerDivider={true} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={null} - headerProps={{ background: "surface" }} - footer={null} - minHeight="100vh" - style={{}} - > - <NextBlock - prefix="next-" - placeholderStyle={{ height: "100%" }} - noPadding={false} - noBorder={false} - background="surface" - layoutmode="O" - colSpan={12} - rowSpan={1} - childTotalColumns={12} - > - <NextBlockCell - title="" - prefix="next-" - placeholderStyle={{ height: "100%" }} - layoutmode="O" - childTotalColumns={12} - isAutoContainer={true} - colSpan={12} - rowSpan={1} - > - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - style={{ marginBottom: "24px" }} - > - <Steps current={__$$eval(() => this.state.currentStep)}> - <Steps.Step title="版本申请" description="" /> - <Steps.Step title="机器配置" subTitle="" description="" /> - <Steps.Step title="项目审批" description="" /> - </Steps> - </NextP> - {!!__$$eval(() => this.state.currentStep === 0) && ( - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - full={true} - flex={true} - style={{ display: "flex", justifyContent: "center" }} - > - <Form - labelCol={{ span: 10 }} - wrapperCol={{ span: 10 }} - onFinish={function () { - this.onFinishFirst.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - style={{ - display: "flex", - flexDirection: "column", - width: "600px", - justifyContent: "center", - }} - layout="vertical" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinish", - relatedEventName: "onFinishFirst", - }, - { - type: "componentEvent", - name: "onValuesChange", - relatedEventName: "onValuesChange", - }, - ], - eventList: [ - { name: "onFinish", disabled: true }, - { name: "onFinishFailed", disabled: false }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: true }, - ], - }} - initialValues={__$$eval( - () => this.state.customerProjectInfo - )} - onValuesChange={function () { - this.onValuesChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - {!!false && ( - <Form.Item - label="" - style={{ width: "600px" }} - colon={false} - name="id" - > - <Input - placeholder="" - style={{ width: "600px" }} - bordered={false} - disabled={true} - /> - </Form.Item> - )} - <Form.Item - label="版本类型选择" - name="projectVersionTypeArray" - initialValue="" - labelAlign="left" - colon={false} - required={true} - style={{ flexDirection: "column", width: "600px" }} - requiredobj={{ - required: true, - message: "请选择版本类型", - }} - > - <Checkbox.Group - options={[ - { label: "基础版本", value: "3" }, - { label: "AR导航", value: "1" }, - { label: "货车导航", value: "2" }, - { label: "UI定制", value: "4", disabled: false }, - ]} - style={{ width: "600px" }} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - /> - </Form.Item> - <Form.Item - label="版本线选择" - labelAlign="left" - colon={false} - required={true} - style={{ width: "600px" }} - name="versionLine" - requiredobj={{ required: true, message: "请选择版本线" }} - extra="" - > - <Select - style={{ width: "600px" }} - options={__$$eval(() => this.state.versionLinesArray)} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - placeholder="请选择版本线" - /> - </Form.Item> - <Form.Item - label="项目名称" - colon={false} - required={true} - style={{ display: "flex" }} - labelAlign="left" - extra="" - name="systemProjectName" - requiredobj={{ - required: true, - message: "请按格式填写项目名称", - }} - typeobj={{ - type: "string", - message: - "请输入项目名称,格式:公司简称-产品名称-版本类型", - }} - lenobj={{ - max: 100, - message: "项目名称不能超过100个字符", - }} - > - <Input - placeholder="公司简称-产品名称-版本类型" - style={{ width: "600px" }} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - /> - </Form.Item> - <Form.Item - label="预期交付时间" - style={{ width: "600px" }} - colon={false} - required={true} - name="expectedTime" - labelAlign="left" - requiredobj={{ - required: true, - message: "请填写预期交付时间", - }} - > - <DatePicker - style={{ width: "600px" }} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - /> - </Form.Item> - <Form.Item - label="预期出货量" - style={{ width: "600px" }} - required={true} - requiredobj={{ - required: true, - message: "请填写预期出货量", - }} - name="expectedNum" - labelAlign="left" - colon={false} - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="单位(台)使用该版本的机器数量+预计出货量,请如实填写" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - min={0} - size="middle" - /> - </Form.Item> - <Form.Item - wrapperCol={{ offset: "" }} - style={{ - flexDirection: "row", - alignItems: "baseline", - justifyContent: "space-between", - width: "600px", - display: "block", - }} - labelAlign="left" - colon={false} - > - <Button - style={{ margin: "0px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickFirstBack", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickFirstBack.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 返回 - </Button> - <Button - type="primary" - htmlType="submit" - style={{ - boxShadow: "rgba(31, 56, 88, 0.2) 0px 0px 0px 0px", - float: "right", - }} - __events={{ - eventDataList: [], - eventList: [{ name: "onClick", disabled: false }], - }} - > - 下一步 - </Button> - </Form.Item> - </Form> - </NextP> - )} - {!!__$$eval(() => this.state.currentStep === 1) && ( - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - full={true} - flex={true} - style={{ display: "flex", justifyContent: "center" }} - > - <Form - labelCol={{ span: 10 }} - wrapperCol={{ span: 10 }} - onFinish={function () { - this.onFinishSecond.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - style={{ - display: "flex", - flexDirection: "column", - width: "600px", - justifyContent: "center", - height: "800px", - }} - layout="vertical" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinish", - relatedEventName: "onFinishSecond", - }, - { - type: "componentEvent", - name: "onValuesChange", - relatedEventName: "onValuesChange", - }, - ], - eventList: [ - { name: "onFinish", disabled: true }, - { name: "onFinishFailed", disabled: false }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: true }, - ], - }} - initialValues={__$$eval( - () => this.state.customerProjectInfo - )} - onValuesChange={function () { - this.onValuesChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - <Form.Item - label="设备类型选择" - labelAlign="left" - colon={false} - required={true} - style={{ width: "600px" }} - name="projectModal" - requiredobj={{ - required: true, - message: "请选择设备类型", - }} - > - <Select - style={{ width: "600px" }} - options={__$$eval(() => this.state.projectModalsArray)} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - placeholder="请选择设备类型" - /> - </Form.Item> - <Form.Item - label="屏幕分辨率宽" - style={{ width: "600px" }} - name="displayWidth" - colon={false} - required={true} - requiredobj={{ - required: true, - message: "请输入屏幕分辨率宽", - }} - labelAlign="left" - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="例如1280" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - min={0} - /> - </Form.Item> - <Form.Item - label="屏幕分辨率高" - style={{ width: "600px" }} - labelAlign="left" - colon={false} - name="displayHeight" - required={true} - requiredobj={{ - required: true, - message: "请输入屏幕分辨率高", - }} - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="例如720" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - min={0} - /> - </Form.Item> - <Form.Item - label="屏幕尺寸(inch)" - style={{ width: "600px" }} - name="displayInch" - labelAlign="left" - required={true} - colon={false} - requiredobj={{ - required: true, - message: "请输入屏幕尺寸", - }} - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="请输入尺寸" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - min={0} - /> - </Form.Item> - <Form.Item - label="屏幕DPI" - style={{ width: "600px" }} - labelAlign="left" - colon={false} - required={false} - name="displayDpi" - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="UI定制项目必填" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - min={0} - /> - </Form.Item> - <Form.Item - label="芯片名称" - colon={false} - required={true} - style={{ display: "flex" }} - labelAlign="left" - extra="" - name="mainSoc" - requiredobj={{ - required: true, - message: "请输入芯片名称", - }} - lenobj={{ max: 50, message: "芯片名称不能超过50个字符" }} - > - <Input - placeholder="请输入芯片名称" - style={{ width: "600px" }} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - /> - </Form.Item> - <Form.Item - label="芯片核数" - style={{ width: "600px" }} - required={true} - requiredobj={{ - required: true, - message: "请输入芯片核数", - }} - name="cpuCoreNum" - labelAlign="left" - colon={false} - > - <InputNumber - value={3} - style={{ width: "600px" }} - placeholder="请输入芯片核数" - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - defaultValue="" - min={0} - /> - </Form.Item> - <Form.Item - label="指令集" - style={{ width: "600px" }} - required={true} - requiredobj={{ required: true, message: "请选择指令集" }} - name="instructions" - colon={false} - > - <Select - style={{ width: "600px" }} - options={__$$eval(() => this.state.instructionsArray)} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - /> - </Form.Item> - <Form.Item - label="系统版本" - labelAlign="left" - colon={false} - required={true} - style={{ width: "600px" }} - name="osVersion" - requiredobj={{ - required: true, - message: "请选择系统版本", - }} - > - <Select - style={{ width: "600px" }} - options={__$$eval(() => this.state.osVersionsArray)} - disabled={__$$eval( - () => - this.state.customerProjectInfo.id > 0 && - !this.state.isModifyStatus - )} - placeholder="请选择系统版本" - /> - </Form.Item> - <Form.Item - wrapperCol={{ offset: "" }} - style={{ - flexDirection: "row", - width: "600px", - display: "flex", - }} - > - <Button - style={{ marginLeft: "0" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickSecondBack", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickSecondBack.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 返回 - </Button> - <Button - type="primary" - htmlType="submit" - style={{ float: "right", marginLeft: "20px" }} - loading={__$$eval( - () => - this.state.LOADING_ADD_OR_UPDATE_CUSTOMER_PROJECT - )} - > - {__$$eval(() => this.state.secondCommitText)} - </Button> - <Button - type="primary" - htmlType="submit" - style={{ marginLeft: "0px", float: "right" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickPreSecond", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickPreSecond.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 上一步 - </Button> - </Form.Item> - </Form> - </NextP> - )} - {!!__$$eval(() => this.state.currentStep === 2) && ( - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - full={true} - flex={true} - style={{ display: "flex", justifyContent: "center" }} - > - <Form - labelCol={{ span: 10 }} - wrapperCol={{ span: 10 }} - onFinishFailed={function () { - this.onFinishFailed.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - style={{ - display: "flex", - flexDirection: "column", - width: "600px", - justifyContent: "center", - }} - layout="vertical" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinishFailed", - relatedEventName: "onFinishFailed", - }, - ], - eventList: [ - { name: "onFinish", disabled: false }, - { name: "onFinishFailed", disabled: true }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: false }, - ], - }} - > - <Form.Item label=""> - <Steps - current={1} - style={{ - width: "600px", - display: "flex", - justifyContent: "space-around", - alignItems: "center", - height: "300px", - }} - labelPlacement="horizontal" - direction="vertical" - > - <Steps.Step - title="提交完成" - description="" - style={{ width: "200px" }} - /> - <Steps.Step - title={__$$eval(() => this.state.thirdAuditText)} - subTitle="" - description="" - style={{ width: "200px" }} - /> - </Steps> - </Form.Item> - <Form.Item - wrapperCol={{ offset: "" }} - style={{ - flexDirection: "row", - width: "600px", - display: "flex", - }} - > - <Button - style={{ marginLeft: "0" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickThirdBack", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickThirdBack.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 返回 - </Button> - <Button - type="primary" - htmlType="submit" - style={{ float: "right", marginLeft: "20px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickModifyThird", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickModifyThird.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - {__$$eval(() => this.state.thirdButtonText)} - </Button> - {!!__$$eval( - () => this.state.customerProjectInfo.status > 2 - ) && ( - <Button - type="primary" - htmlType="submit" - style={{ marginLeft: "0px", float: "right" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onClickPreThird", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onClickPreThird.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 上一步 - </Button> - )} - </Form.Item> - </Form> - </NextP> - )} - </NextBlockCell> - </NextBlock> - </NextPage> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js deleted file mode 100644 index ce50d58b70..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json deleted file mode 100644 index 97a6334be2..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-http-handler": "latest", - "@alilc/lowcode-components": "^1.0.0" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx deleted file mode 100644 index 06bc59021f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx +++ /dev/null @@ -1,110 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Page, Table } from "@alilc/lowcode-components"; - -import { createHttpHandler as __$$createHttpRequestHandler } from "@alilc/lowcode-datasource-http-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Example$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { http: __$$createHttpRequestHandler() }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - __$$i18n._inject2(this); - - this.state = {}; - } - - $ = () => null; - - $$ = () => []; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - id: "userList", - type: "http", - description: "用户列表", - options: function () { - return { - uri: "https://api.example.com/user/list", - }; - }, - isInit: function () { - return undefined; - }, - }, - ], - }; - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div> - <Table - dataSource={__$$eval(() => this.dataSourceMap["userList"])} - columns={[ - { dataIndex: "name", title: "姓名" }, - { dataIndex: "age", title: "年龄" }, - ]} - /> - </div> - ); - } -} - -export default Example$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js deleted file mode 100644 index a381684b71..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Example from "@/pages/Example"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Example, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json deleted file mode 100644 index 60b0e37f11..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "@alilc/lowcode-datasource-jsonp-handler": "latest", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/components/Index/index.jsx b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/components/Index/index.jsx deleted file mode 100644 index 47bca5a63e..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/components/Index/index.jsx +++ /dev/null @@ -1,120 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { Switch } from "@alifd/next"; - -import { createJsonpHandler as __$$createJsonpRequestHandler } from "@alilc/lowcode-datasource-jsonp-handler"; - -import { create as __$$createDataSourceEngine } from "@alilc/lowcode-datasource-engine/runtime"; - -import utils from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -class Index$$Page extends React.Component { - _context = this; - - _dataSourceConfig = this._defineDataSourceConfig(); - _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { - runtimeConfig: true, - requestHandlersMap: { jsonp: __$$createJsonpRequestHandler() }, - }); - - get dataSourceMap() { - return this._dataSourceEngine.dataSourceMap || {}; - } - - reloadDataSource = async () => { - await this._dataSourceEngine.reloadDataSource(); - }; - - constructor(props, context) { - super(props); - - this.utils = utils; - - __$$i18n._inject2(this); - - this.state = {}; - } - - $ = () => null; - - $$ = () => []; - - _defineDataSourceConfig() { - const _this = this; - return { - list: [ - { - id: "todos", - isInit: function () { - return true; - }, - type: "jsonp", - options: function () { - return { - method: "GET", - uri: "https://a0ee9135-6a7f-4c0f-a215-f0f247ad907d.mock.pstmn.io", - }; - }, - dataHandler: function dataHandler(data) { - return data.data; - }, - }, - ], - }; - } - - componentDidMount() { - this._dataSourceEngine.reloadDataSource(); - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div> - <div> - {__$$evalArray(() => this.dataSourceMap.todos.data).map( - (item, index) => - ((__$$context) => ( - <div> - <Switch - checkedChildren="开" - unCheckedChildren="关" - checked={__$$eval(() => item.done)} - /> - </div> - ))(__$$createChildContext(__$$context, { item, index })) - )} - </div> - </div> - ); - } -} - -export default Index$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js deleted file mode 100644 index accf34e86f..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js +++ /dev/null @@ -1,11 +0,0 @@ -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json deleted file mode 100644 index 64912076d7..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "undefined": "*", - "@alilc/antd-lowcode-materials": "0.9.4", - "@alife/mc-assets-1935": "0.1.42", - "@alife/container": "0.3.7" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index be57351782..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,817 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { - Modal, - Button, - Typography, - Form, - Select, - Input, - ConfigProvider, - Tooltip, - Empty, -} from "@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js"; - -import { - AliAutoDiv, - AliAutoSearchTable, -} from "@alife/mc-assets-1935/build/lowcode/index.js"; - -import { - Page as NextPage, - Block as NextBlock, - P as NextP, -} from "@alife/container/lib/index.js"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const AliAutoDivDefault = AliAutoDiv.default; - -const AliAutoSearchTableDefault = AliAutoSearchTable.default; - -const NextBlockCell = NextBlock.Cell; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { - pkgs: [], - total: 0, - isSearch: false, - projects: [], - results: [], - resultVisible: false, - }; - - this.__jp__init(); - - this.statusDesc = { - 0: "失败", - 1: "成功", - 2: "构建中", - 3: "构建超时", - }; - this.pageParams = {}; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - componentDidUpdate(prevProps, prevState, snapshot) {} - - componentWillUnmount() {} - - __jp__init() { - /*...*/ - } - - __jp__initRouter() { - if (window.arsenal) { - this.$router = new window.jianpin.ArsenalRouter({ - app: this.props.microApp, - }); - } else { - this.$router = new window.jianpin.ArsenalRouter(); - } - } - - __jp__initDataSource() { - /*...*/ - } - - __jp__initEnv() { - /*...*/ - } - - __jp__initConfig() { - /*...*/ - } - - __jp__initUtils() { - this.$utils = { - message: window.jianpin.utils.message, - axios: window.jianpin.utils.axios, - moment: window.jianpin.utils.moment, - }; - } - - fetchPkgs() { - /*...*/ - } - - onPageChange(pageIndex, pageSize) { - this.pageParams = { - pageIndex, - pageSize, - }; - this.fetchPkgs(); - } - - renderTime(time) { - return this.$utils.moment(time).format("YYYY-MM-DD HH:mm"); - } - - renderUserName(user) { - return user.user_name; - } - - reload() { - /*...*/ - } - - handleResult() { - /*...*/ - } - - handleDetail() { - // 跳转详情页面 TODO - } - - onResultCancel() { - this.setState({ - resultVisible: false, - }); - } - - formatResult(item) { - if (!item) { - return "暂无结果"; - } - - const { channel, plat, version, status } = item; - return [channel, plat, version, status].join("-"); - } - - handleDownload() { - /*...*/ - } - - onFinish() { - /*...*/ - } - - componentDidMount() { - this.$ds.resolve("PROJECTS", { - params: { - size: 5000, - }, - }); // if (this.state.init === false) { - // this.setState({ - // init: true, - // }); - // } - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div - ref={this._refsManager.linkRef("outterView")} - style={{ height: "100%" }} - > - <Modal - title="查看结果" - visible={__$$eval(() => this.state.resultVisible)} - footer={ - <Button - type="primary" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onResultCancel", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onResultCancel.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 确定 - </Button> - } - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onCancel", - relatedEventName: "onResultCancel", - }, - ], - eventList: [ - { name: "onCancel", disabled: true }, - { name: "onOk", disabled: false }, - ], - }} - onCancel={function () { - this.onResultCancel.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - width="720px" - centered={true} - > - {__$$evalArray(() => this.state.results).map((item, index) => - ((__$$context) => ( - <AliAutoDivDefault style={{ width: "100%" }}> - {!!__$$eval( - () => - __$$context.state.results && - __$$context.state.results.length > 0 - ) && ( - <AliAutoDivDefault - style={{ - width: "100%", - textAlign: "left", - marginBottom: "10px", - }} - > - <Button - type="primary" - size="small" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleDownload", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.handleDownload.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(__$$context)} - > - 下载全部 - </Button> - </AliAutoDivDefault> - )} - <Typography.Text> - {__$$eval(() => __$$context.formatResult(item))} - </Typography.Text> - {!!__$$eval(() => item.download_link) && ( - <Typography.Link - href={__$$eval(() => item.download_link)} - target="_blank" - > - {" "} - - 点击下载 - </Typography.Link> - )} - {!!__$$eval(() => item.release_notes) && ( - <Typography.Link - href={__$$eval(() => item.release_notes)} - target="_blank" - > - {" "} - - 跳转发布节点 - </Typography.Link> - )} - </AliAutoDivDefault> - ))(__$$createChildContext(__$$context, { item, index })) - )} - </Modal> - <NextPage - columns={12} - headerDivider={true} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={null} - headerProps={{ background: "surface" }} - footer={null} - minHeight="100vh" - > - <NextBlock - prefix="next-" - placeholderStyle={{ height: "100%" }} - noPadding={false} - noBorder={false} - background="surface" - layoutmode="O" - colSpan={12} - rowSpan={1} - childTotalColumns={12} - > - <NextBlockCell - title="" - prefix="next-" - placeholderStyle={{ height: "100%" }} - layoutmode="O" - childTotalColumns={12} - isAutoContainer={true} - colSpan={12} - rowSpan={1} - > - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - full={true} - flex={true} - > - <Form - labelCol={{ span: 10 }} - wrapperCol={{ span: 14 }} - onFinish={function () { - this.onFinish.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - layout="inline" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinish", - relatedEventName: "onFinish", - }, - ], - eventList: [ - { name: "onFinish", disabled: true }, - { name: "onFinishFailed", disabled: false }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: false }, - ], - }} - > - <Form.Item label="项目名称/渠道号" name="channel_id"> - <Select - style={{ width: "280px" }} - options={__$$eval(() => this.state.projects)} - showArrow={true} - tokenSeparators={[]} - showSearch={true} - /> - </Form.Item> - <Form.Item label="版本号" name="buildId"> - <Input - placeholder="请输入" - style={{ width: "280px" }} - size="middle" - /> - </Form.Item> - <Form.Item label="构建人" name="user_id"> - <Select - style={{ width: 200 }} - options={[ - { label: "A", value: "A" }, - { label: "B", value: "B" }, - { label: "C", value: "C" }, - ]} - showSearch={true} - /> - </Form.Item> - <Form.Item label="ID" name="id"> - <Input placeholder="请输入" style={{ width: "160px" }} /> - </Form.Item> - <Form.Item wrapperCol={{ offset: 6 }}> - <Button type="primary" htmlType="submit"> - 查询 - </Button> - </Form.Item> - </Form> - </NextP> - </NextBlockCell> - </NextBlock> - <NextBlock childTotalColumns={12}> - <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - <ConfigProvider locale="zh-CN"> - {!!__$$eval( - () => - !this.state.isSearch || - (this.state.isSearch && this.state.pkgs.length > 0) - ) && ( - <AliAutoSearchTableDefault - rowKey="key" - dataSource={__$$eval(() => this.state.pkgs)} - columns={[ - { - title: "ID", - dataIndex: "id", - key: "name", - width: 80, - }, - { - title: "渠道号", - dataIndex: "channels", - key: "age", - width: 142, - render: (text, record, index) => - ((__$$context) => - __$$evalArray(() => text.split(",")).map( - (item, index) => - ((__$$context) => ( - <Typography.Text - style={{ display: "block" }} - > - {__$$eval(() => item)} - </Typography.Text> - ))( - __$$createChildContext(__$$context, { - item, - index, - }) - ) - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - }, - { - title: "版本号", - dataIndex: "dic_version", - key: "address", - render: (text, record, index) => - ((__$$context) => ( - <Tooltip - title={__$$evalArray(() => text || []).map( - (item, index) => - ((__$$context) => ( - <Typography.Text - style={{ - display: "block", - color: "#FFFFFF", - }} - > - {__$$eval( - () => - item.channelId + - " / " + - item.version - )} - </Typography.Text> - ))( - __$$createChildContext(__$$context, { - item, - index, - }) - ) - )} - > - <Typography.Text> - {__$$eval(() => text[0].version)} - </Typography.Text> - </Tooltip> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 120, - }, - { title: "构建Job", dataIndex: "job_name", width: 180 }, - { - title: "构建类型", - dataIndex: "packaging_type", - width: 94, - }, - { - title: "构建状态", - dataIndex: "status", - render: (text, record, index) => - ((__$$context) => [ - <Typography.Text> - {__$$eval(() => __$$context.statusDesc[text])} - </Typography.Text>, - !!__$$eval(() => text === 2) && ( - <Icon - type="SyncOutlined" - size={16} - spin={true} - style={{ marginLeft: "10px" }} - /> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 100, - }, - { - title: "构建时间", - dataIndex: "start_time", - render: function () { - return this.renderTime.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - width: 148, - }, - { - title: "构建人", - dataIndex: "user", - render: function () { - return this.renderUserName.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - width: 80, - }, - { - title: "Jenkins 链接", - dataIndex: "jenkins_link", - render: (text, record, index) => - ((__$$context) => [ - !!__$$eval(() => text) && ( - <Typography.Link - href={__$$eval(() => text)} - target="_blank" - > - 查看 - </Typography.Link> - ), - !!__$$eval(() => !text) && ( - <Typography.Text>暂无</Typography.Text> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 120, - }, - { - title: "测试平台链接", - dataIndex: "is_run_testing", - width: 120, - render: (text, record, index) => - ((__$$context) => [ - !!__$$eval(() => text) && ( - <Typography.Link - href="http://rivermap.alibaba.net/dashboard/testExecute" - target="_blank" - > - 查看 - </Typography.Link> - ), - !!__$$eval(() => !text) && ( - <Typography.Text>暂无</Typography.Text> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - }, - { title: "触发源", dataIndex: "source", width: 120 }, - { - title: "详情", - dataIndex: "id", - render: (text, record, index) => - ((__$$context) => ( - <Button - type="link" - size="small" - style={{ padding: "0px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleDetail", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.handleDetail.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - > - 查看 - </Button> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 80, - fixed: "right", - }, - { - title: "结果", - dataIndex: "id", - render: (text, record, index) => - ((__$$context) => ( - <Button - type="link" - size="small" - style={{ padding: "0px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleResult", - paramStr: "this.text", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.handleResult.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - ghost={false} - href={__$$eval(() => text)} - > - 查看 - </Button> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 80, - fixed: "right", - }, - { - title: "重新执行", - dataIndex: "id", - width: 92, - render: (text, record, index) => - ((__$$context) => ( - <Button - type="text" - children="" - icon={ - <Icon - type="ReloadOutlined" - size={14} - color="#0593d3" - style={{ - padding: "3px", - border: "1px solid #0593d3", - borderRadius: "14px", - cursor: "pointer", - height: "22px", - }} - spin={false} - /> - } - shape="circle" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "reload", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.reload.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - /> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - fixed: "right", - }, - ]} - actions={[]} - pagination={{ - total: __$$eval(() => this.state.total), - defaultPageSize: 8, - onPageChange: function () { - return this.onPageChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - }} - scrollX={1200} - /> - )} - </ConfigProvider> - </NextP> - </NextBlockCell> - </NextBlock> - <NextBlock childTotalColumns={12}> - <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - {!!__$$eval( - () => this.state.pkgs.length < 1 && this.state.isSearch - ) && <Empty description="暂无数据" />} - </NextP> - </NextBlockCell> - </NextBlock> - </NextPage> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js deleted file mode 100644 index ce50d58b70..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json deleted file mode 100644 index 5d8d509e72..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "icejs-demo-app", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "react-router": "^5.2.1", - "@alifd/theme-design-pro": "^0.x", - "intl-messageformat": "^9.3.6", - "@ice/store": "^1.4.3", - "@loadable/component": "^5.15.2", - "@alilc/lowcode-datasource-engine": "latest", - "undefined": "*", - "@alilc/antd-lowcode-materials": "0.11.0", - "@alife/mc-assets-1935": "0.1.43", - "@alife/container": "0.3.7" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss deleted file mode 100644 index cc339ce97b..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss +++ /dev/null @@ -1,6 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js deleted file mode 100644 index 60e05915d9..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js +++ /dev/null @@ -1,77 +0,0 @@ -const i18nConfig = {}; - -let locale = - typeof navigator === "object" && typeof navigator.language === "string" - ? navigator.language - : "zh-CN"; - -const getLocale = () => locale; - -const setLocale = (target) => { - locale = target; -}; - -const isEmptyVariables = (variables) => - (Array.isArray(variables) && variables.length === 0) || - (typeof variables === "object" && - (!variables || Object.keys(variables).length === 0)); - -// 按低代码规范里面的要求进行变量替换 -const format = (msg, variables) => - typeof msg === "string" - ? msg.replace(/\$\{(\w+)\}/g, (match, key) => variables?.[key] ?? "") - : msg; - -const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { - const msg = - i18nConfig[locale]?.[id] ?? - i18nConfig[locale.replace("-", "_")]?.[id] ?? - defaultMessage; - if (msg == null) { - console.warn("[i18n]: unknown message id: %o (locale=%o)", id, locale); - return fallback === undefined ? `${id}` : fallback; - } - - return format(msg, variables); -}; - -const i18n = (id, params) => { - return i18nFormat({ id }, params); -}; - -// 将国际化的一些方法注入到目标对象&上下文中 -const _inject2 = (target) => { - target.i18n = i18n; - target.getLocale = getLocale; - target.setLocale = (locale) => { - setLocale(locale); - target.forceUpdate(); - }; - target._i18nText = (t) => { - // 优先取直接传过来的语料 - const localMsg = t[locale] ?? t[String(locale).replace("-", "_")]; - if (localMsg != null) { - return format(localMsg, t.params); - } - - // 其次用项目级别的 - const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); - if (projectMsg != null) { - return projectMsg; - } - - // 兜底用 use 指定的或默认语言的 - return format(t[t.use || "zh_CN"] ?? t.en_US, t.params); - }; - - // 注入到上下文中去 - if (target._context && target._context !== target) { - Object.assign(target._context, { - i18n, - getLocale, - setLocale: target.setLocale, - }); - } -}; - -export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx deleted file mode 100644 index 92c9605346..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx +++ /dev/null @@ -1,973 +0,0 @@ -// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 -// 例外:react 框架的导出名和各种组件名除外。 -import React from "react"; - -import { - Modal, - Button, - Typography, - Form, - Select, - Input, - Tooltip, - Icon, - Empty, -} from "@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js"; - -import { - AliAutoDiv, - AliAutoSearchTable, -} from "@alife/mc-assets-1935/build/lowcode/index.js"; - -import { - Page as NextPage, - Block as NextBlock, - P as NextP, -} from "@alife/container/lib/index.js"; - -import utils, { RefsManager } from "../../utils"; - -import * as __$$i18n from "../../i18n"; - -import "./index.css"; - -const AliAutoDivDefault = AliAutoDiv.default; - -const AliAutoSearchTableDefault = AliAutoSearchTable.default; - -const NextBlockCell = NextBlock.Cell; - -class Test$$Page extends React.Component { - _context = this; - - constructor(props, context) { - super(props); - - this.utils = utils; - - this._refsManager = new RefsManager(); - - __$$i18n._inject2(this); - - this.state = { - pkgs: [], - total: 0, - isSearch: false, - projects: [], - results: [], - resultVisible: false, - userOptions: [], - searchValues: { user_id: "", channel_id: "" }, - }; - - this.__jp__init(); - - this.statusDesc = { - 0: "失败", - 1: "成功", - 2: "构建中", - 3: "构建超时", - }; - this.pageParams = {}; - this.searchParams = {}; - this.userTimeout = null; - this.currentUser = null; - this.notFoundContent = null; - this.projectTimeout = null; - this.currentProject = null; - } - - $ = (refName) => { - return this._refsManager.get(refName); - }; - - $$ = (refName) => { - return this._refsManager.getAll(refName); - }; - - componentDidUpdate(prevProps, prevState, snapshot) {} - - componentWillUnmount() {} - - __jp__init() { - /*...*/ - } - - __jp__initRouter() { - /*...*/ - } - - __jp__initDataSource() { - /*...*/ - } - - __jp__initEnv() { - /*...*/ - } - - __jp__initConfig() { - /*...*/ - } - - __jp__initUtils() { - /*...*/ - } - - setSearchItem() { - /*...*/ - } - - fetchProject() { - /*...*/ - } - - handleProjectSearch() { - /*...*/ - } - - handleProjectChange(id) { - this.setSearchItem({ - channel_id: id, - }); - } - - fetchUser() { - /*...*/ - } - - handleUserSearch() { - /*...*/ - } - - handleUserChange(user) { - console.log("debug user", user); - this.setSearchItem({ - user_id: user, - }); - } - - fetchPkgs() { - /*...*/ - } - - onPageChange(pageIndex, pageSize) { - this.pageParams = { - pageIndex, - pageSize, - }; - this.fetchPkgs(); - } - - renderTime(time) { - return this.$utils.moment(time).format("YYYY-MM-DD HH:mm"); - } - - renderUserName(user) { - return user.user_name; - } - - reload() { - /*...*/ - } - - handleResult() { - /*...*/ - } - - handleDetail() { - /*...*/ - } - - onResultCancel() { - /*...*/ - } - - formatResult() { - /*...*/ - } - - handleDownload() { - /*...*/ - } - - onFinish() { - /*...*/ - } - - componentDidMount() { - this.$ds.resolve("PROJECTS"); - - if (this.userTimeout) { - clearTimeout(this.userTimeout); - this.userTimeout = null; - } - - if (this.projectTimeout) { - clearTimeout(this.projectTimeout); - this.projectTimeout = null; - } - } - - render() { - const __$$context = this._context || this; - const { state } = __$$context; - return ( - <div - ref={this._refsManager.linkRef("outterView")} - style={{ height: "100%" }} - > - <Modal - title="查看结果" - visible={__$$eval(() => this.state.resultVisible)} - footer={ - <Button - type="primary" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "onResultCancel", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.onResultCancel.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 确定 - </Button> - } - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onCancel", - relatedEventName: "onResultCancel", - }, - ], - eventList: [ - { name: "onCancel", disabled: true }, - { name: "onOk", disabled: false }, - ], - }} - onCancel={function () { - this.onResultCancel.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - width="720px" - centered={true} - closable={true} - keyboard={true} - mask={true} - maskClosable={true} - > - <AliAutoDivDefault style={{ width: "100%" }}> - {!!__$$eval( - () => this.state.results && this.state.results.length > 0 - ) && ( - <AliAutoDivDefault - style={{ - width: "100%", - textAlign: "left", - marginBottom: "16px", - }} - > - <Button - type="primary" - size="small" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleDownload", - }, - ], - eventList: [{ name: "onClick", disabled: true }], - }} - onClick={function () { - this.handleDownload.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - > - 下载全部 - </Button> - </AliAutoDivDefault> - )} - {__$$evalArray(() => this.state.results).map((item, index) => - ((__$$context) => ( - <AliAutoDivDefault style={{ width: "100%", marginTop: "10px" }}> - <Typography.Text> - {__$$eval(() => __$$context.formatResult(item))} - </Typography.Text> - {!!__$$eval(() => item.download_link) && ( - <Typography.Link - href={__$$eval(() => item.download_link)} - target="_blank" - > - {" "} - - 点击下载 - </Typography.Link> - )} - {!!__$$eval(() => item.release_notes) && ( - <Typography.Link - href={__$$eval(() => item.release_notes)} - target="_blank" - > - {" "} - - 跳转发布节点 - </Typography.Link> - )} - </AliAutoDivDefault> - ))(__$$createChildContext(__$$context, { item, index })) - )} - </AliAutoDivDefault> - </Modal> - <NextPage - columns={12} - headerDivider={true} - placeholderStyle={{ gridRowEnd: "span 1", gridColumnEnd: "span 12" }} - placeholder="页面主体内容:拖拽Block布局组件到这里" - header={null} - headerProps={{ background: "surface", style: { padding: "" } }} - footer={null} - minHeight="100vh" - contentProps={{ noPadding: false, background: "transparent" }} - > - <NextBlock childTotalColumns={12}> - <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - <AliAutoDivDefault style={{ width: "100%", display: "flex" }}> - <AliAutoDivDefault style={{ flex: "1" }}> - <Form - labelCol={{ span: 10 }} - wrapperCol={{ span: 14 }} - onFinish={function () { - this.onFinish.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - name="basic" - layout="inline" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onFinish", - relatedEventName: "onFinish", - }, - ], - eventList: [ - { name: "onFinish", disabled: true }, - { name: "onFinishFailed", disabled: false }, - { name: "onFieldsChange", disabled: false }, - { name: "onValuesChange", disabled: false }, - ], - }} - colon={true} - labelAlign="right" - preserve={true} - scrollToFirstError={true} - size="middle" - values={__$$eval(() => this.state.searchValues)} - > - <Form.Item - label="项目名称/渠道号" - name="channel_id" - labelAlign="right" - colon={true} - > - <Select - style={{ width: "320px" }} - options={__$$eval(() => this.state.projects)} - showArrow={false} - tokenSeparators={[]} - showSearch={true} - defaultActiveFirstOption={true} - size="middle" - bordered={true} - filterOption={true} - optionFilterProp="label" - allowClear={true} - placeholder="请输入项目名称/渠道号" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onChange", - relatedEventName: "handleProjectChange", - }, - { - type: "componentEvent", - name: "onSearch", - relatedEventName: "handleProjectSearch", - }, - ], - eventList: [ - { name: "onBlur", disabled: false }, - { name: "onChange", disabled: true }, - { name: "onDeselect", disabled: false }, - { name: "onFocus", disabled: false }, - { name: "onInputKeyDown", disabled: false }, - { name: "onMouseEnter", disabled: false }, - { name: "onMouseLeave", disabled: false }, - { name: "onPopupScroll", disabled: false }, - { name: "onSearch", disabled: true }, - { name: "onSelect", disabled: false }, - { - name: "onDropdownVisibleChange", - disabled: false, - }, - ], - }} - onChange={function () { - this.handleProjectChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - onSearch={function () { - this.handleProjectSearch.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - /> - </Form.Item> - <Form.Item label="版本号" name="buildId"> - <Input - placeholder="请输入版本号" - style={{ width: "180px" }} - size="middle" - bordered={true} - /> - </Form.Item> - <Form.Item label="构建人" name="user_id"> - <Select - style={{ width: "210px" }} - options={__$$eval(() => this.state.userOptions)} - showSearch={true} - defaultActiveFirstOption={false} - size="middle" - bordered={true} - filterOption={true} - optionFilterProp="label" - notFoundContent={__$$eval( - () => this.userNotFoundContent - )} - showArrow={false} - placeholder="请输入构建人" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onChange", - relatedEventName: "handleUserChange", - }, - { - type: "componentEvent", - name: "onSearch", - relatedEventName: "handleUserSearch", - }, - ], - eventList: [ - { name: "onBlur", disabled: false }, - { name: "onChange", disabled: true }, - { name: "onDeselect", disabled: false }, - { name: "onFocus", disabled: false }, - { name: "onInputKeyDown", disabled: false }, - { name: "onMouseEnter", disabled: false }, - { name: "onMouseLeave", disabled: false }, - { name: "onPopupScroll", disabled: false }, - { name: "onSearch", disabled: true }, - { name: "onSelect", disabled: false }, - { - name: "onDropdownVisibleChange", - disabled: false, - }, - ], - }} - onChange={function () { - this.handleUserChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - onSearch={function () { - this.handleUserSearch.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this)} - allowClear={true} - /> - </Form.Item> - <Form.Item - label="ID" - name="id" - labelAlign="right" - colon={true} - > - <Input - placeholder="请输入ID" - style={{ width: "180px" }} - bordered={true} - size="middle" - /> - </Form.Item> - <Form.Item - wrapperCol={{ offset: 6 }} - labelAlign="right" - colon={true} - style={{ flex: "1", textAlign: "right" }} - > - <Button - type="primary" - htmlType="submit" - shape="default" - size="middle" - > - 查询 - </Button> - </Form.Item> - </Form> - </AliAutoDivDefault> - <AliAutoDivDefault style={{}}> - <Button - type="link" - htmlType="button" - shape="default" - size="middle" - > - 新增打包 - </Button> - </AliAutoDivDefault> - </AliAutoDivDefault> - </NextP> - </NextBlockCell> - </NextBlock> - <NextBlock - childTotalColumns={12} - mode="inset" - layoutmode="O" - autolayout="(12|1)" - > - <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - {!!__$$eval( - () => - !this.state.isSearch || - (this.state.isSearch && this.state.pkgs.length > 0) - ) && ( - <AliAutoSearchTableDefault - rowKey="key" - dataSource={__$$eval(() => this.state.pkgs)} - columns={[ - { title: "ID", dataIndex: "id", key: "name", width: 80 }, - { - title: "渠道号", - dataIndex: "channels", - key: "age", - width: 142, - render: (text, record, index) => - ((__$$context) => - __$$evalArray(() => text.split(",")).map( - (item, index) => - ((__$$context) => ( - <Typography.Text style={{ display: "block" }}> - {__$$eval(() => item)} - </Typography.Text> - ))( - __$$createChildContext(__$$context, { - item, - index, - }) - ) - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - }, - { - title: "版本号", - dataIndex: "dic_version", - key: "address", - render: (text, record, index) => - ((__$$context) => ( - <Tooltip - title={__$$evalArray(() => text || []).map( - (item, index) => - ((__$$context) => ( - <Typography.Text - style={{ - display: "block", - color: "#FFFFFF", - }} - > - {__$$eval( - () => - item.channelId + " / " + item.version - )} - </Typography.Text> - ))( - __$$createChildContext(__$$context, { - item, - index, - }) - ) - )} - > - <Typography.Text> - {__$$eval(() => text[0].version)} - </Typography.Text> - </Tooltip> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 120, - }, - { title: "构建Job", dataIndex: "job_name", width: 180 }, - { - title: "构建类型", - dataIndex: "packaging_type", - width: 94, - }, - { - title: "构建状态", - dataIndex: "status", - render: (text, record, index) => - ((__$$context) => [ - <Typography.Text> - {__$$eval(() => __$$context.statusDesc[text])} - </Typography.Text>, - !!__$$eval(() => text === 2) && ( - <Icon - type="SyncOutlined" - size={16} - spin={true} - style={{ marginLeft: "10px" }} - /> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 100, - }, - { - title: "构建时间", - dataIndex: "start_time", - render: function () { - return this.renderTime.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - width: 148, - }, - { - title: "构建人", - dataIndex: "user", - render: function () { - return this.renderUserName.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - width: 80, - }, - { - title: "Jenkins 链接", - dataIndex: "jenkins_link", - render: (text, record, index) => - ((__$$context) => [ - !!__$$eval(() => text) && ( - <Typography.Link - href={__$$eval(() => text)} - target="_blank" - > - 查看 - </Typography.Link> - ), - !!__$$eval(() => !text) && ( - <Typography.Text>暂无</Typography.Text> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 120, - }, - { - title: "测试平台链接", - dataIndex: "is_run_testing", - width: 120, - render: (text, record, index) => - ((__$$context) => [ - !!__$$eval(() => text) && ( - <Typography.Link - href="http://rivermap.alibaba.net/dashboard/testExecute" - target="_blank" - > - 查看 - </Typography.Link> - ), - !!__$$eval(() => !text) && ( - <Typography.Text>暂无</Typography.Text> - ), - ])( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - }, - { title: "触发源", dataIndex: "source", width: 120 }, - { - title: "详情", - dataIndex: "id", - render: (text, record, index) => - ((__$$context) => ( - <Button - type="link" - size="small" - style={{ padding: "0px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleDetail", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.handleDetail.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - > - 查看 - </Button> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 80, - fixed: "right", - }, - { - title: "结果", - dataIndex: "id", - render: (text, record, index) => - ((__$$context) => ( - <Button - type="link" - size="small" - style={{ padding: "0px" }} - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "handleResult", - paramStr: "this.text", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.handleResult.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - ghost={false} - href={__$$eval(() => text)} - > - 查看 - </Button> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - width: 80, - fixed: "right", - }, - { - title: "重新执行", - dataIndex: "id", - width: 92, - render: (text, record, index) => - ((__$$context) => ( - <Button - type="text" - children="" - icon={ - <Icon - type="ReloadOutlined" - size={14} - color="#0593d3" - style={{ - padding: "3px", - border: "1px solid #0593d3", - borderRadius: "14px", - cursor: "pointer", - height: "22px", - }} - spin={false} - /> - } - shape="circle" - __events={{ - eventDataList: [ - { - type: "componentEvent", - name: "onClick", - relatedEventName: "reload", - }, - ], - eventList: [ - { name: "onClick", disabled: true }, - ], - }} - onClick={function () { - this.reload.apply( - this, - Array.prototype.slice - .call(arguments) - .concat([]) - ); - }.bind(__$$context)} - /> - ))( - __$$createChildContext(__$$context, { - text, - record, - index, - }) - ), - fixed: "right", - }, - ]} - actions={[]} - pagination={{ - total: __$$eval(() => this.state.total), - defaultPageSize: 10, - onPageChange: function () { - return this.onPageChange.apply( - this, - Array.prototype.slice.call(arguments).concat([]) - ); - }.bind(this), - defaultPageIndex: 1, - }} - scrollX={1200} - isPagination={true} - /> - )} - </NextP> - </NextBlockCell> - </NextBlock> - <NextBlock - childTotalColumns={12} - mode="inset" - layoutmode="O" - autolayout="(12|1)" - > - <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> - <NextP - wrap={false} - type="body2" - verAlign="middle" - textSpacing={true} - align="left" - flex={true} - > - {!!__$$eval( - () => this.state.pkgs.length < 1 && this.state.isSearch - ) && <Empty description="暂无数据" />} - </NextP> - </NextBlockCell> - </NextBlock> - </NextPage> - </div> - ); - } -} - -export default Test$$Page; - -function __$$eval(expr) { - try { - return expr(); - } catch (error) {} -} - -function __$$evalArray(expr) { - const res = __$$eval(expr); - return Array.isArray(res) ? res : []; -} - -function __$$createChildContext(oldContext, ext) { - const childContext = { - ...oldContext, - ...ext, - }; - childContext.__proto__ = oldContext; - return childContext; -} diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js deleted file mode 100644 index ce50d58b70..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/package.json b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/package.json deleted file mode 100644 index ebb17b143d..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@alifd/scaffold-lite-js", - "version": "0.1.5", - "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", - "dependencies": { - "moment": "^2.24.0", - "react": "^16.4.1", - "react-dom": "^16.4.1", - "@alifd/theme-design-pro": "^0.x", - "@alifd/next": "1.19.18" - }, - "devDependencies": { - "@ice/spec": "^1.0.0", - "build-plugin-fusion": "^0.1.0", - "build-plugin-moment-locales": "^0.1.0", - "eslint": "^6.0.1", - "ice.js": "^1.0.0", - "stylelint": "^13.2.0", - "@ali/build-plugin-ice-def": "^0.1.0" - }, - "scripts": { - "start": "icejs start", - "build": "icejs build", - "lint": "npm run eslint && npm run stylelint", - "eslint": "eslint --cache --ext .js,.jsx ./", - "stylelint": "stylelint ./**/*.scss" - }, - "ideMode": { "name": "ice-react" }, - "iceworks": { "type": "react", "adapter": "adapter-react-v3" }, - "engines": { "node": ">=8.0.0" }, - "repository": { - "type": "git", - "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" - }, - "private": true, - "originTemplate": "@alifd/scaffold-lite-js" -} diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/app.js b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/app.js deleted file mode 100644 index fb01b106b4..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/app.js +++ /dev/null @@ -1,11 +0,0 @@ -import { createApp } from "ice"; - -const appConfig = { - app: { - rootId: "app", - }, - router: { - type: "hash", - }, -}; -createApp(appConfig); diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/constants.js b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/constants.js deleted file mode 100644 index c4a5859ee4..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -const __$$constants = { ENV: "prod", DOMAIN: "xxx.xxx.com" }; - -export default __$$constants; diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/global.scss b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/global.scss deleted file mode 100644 index 2d97c56b09..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/global.scss +++ /dev/null @@ -1,13 +0,0 @@ -// 引入默认全局样式 -@import "@alifd/next/reset.scss"; - -body { - -webkit-font-smoothing: antialiased; -} - -body { - font-size: 12px; -} -.table { - width: 100px; -} diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/routes.js b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/routes.js deleted file mode 100644 index e6b7426d47..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/routes.js +++ /dev/null @@ -1,18 +0,0 @@ -import Test from "@/pages/Test"; - -import BasicLayout from "@/layouts/BasicLayout"; - -const routerConfig = [ - { - path: "/", - component: BasicLayout, - children: [ - { - path: "/", - component: Test, - }, - ], - }, -]; - -export default routerConfig; diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/utils.js b/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/utils.js deleted file mode 100644 index 3d3ca9a81c..0000000000 --- a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/utils.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createRef } from "react"; - -export class RefsManager { - constructor() { - this.refInsStore = {}; - } - - clearNullRefs() { - Object.keys(this.refInsStore).forEach((refName) => { - const filteredInsList = this.refInsStore[refName].filter( - (insRef) => !!insRef.current - ); - if (filteredInsList.length > 0) { - this.refInsStore[refName] = filteredInsList; - } else { - delete this.refInsStore[refName]; - } - }); - } - - get(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName][0].current; - } - - return null; - } - - getAll(refName) { - this.clearNullRefs(); - if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { - return this.refInsStore[refName].map((i) => i.current); - } - - return []; - } - - linkRef(refName) { - const refIns = createRef(); - this.refInsStore[refName] = this.refInsStore[refName] || []; - this.refInsStore[refName].push(refIns); - return refIns; - } -} - -export default {}; diff --git a/modules/code-generator/tests/bugfix/i18n-with-params.schema.json b/modules/code-generator/tests/bugfix/i18n-with-params.schema.json index 88217ab952..227a1d66fa 100644 --- a/modules/code-generator/tests/bugfix/i18n-with-params.schema.json +++ b/modules/code-generator/tests/bugfix/i18n-with-params.schema.json @@ -42,10 +42,10 @@ } ], "i18n": { - "zh_CN": { + "zh-CN": { "greetings.hello": "${name}, 你好!" }, - "en_US": { + "en-US": { "greetings.hello": "Hello, ${name}!" } } diff --git a/modules/code-generator/tests/bugfix/i18n-with-params.test.ts b/modules/code-generator/tests/bugfix/i18n-with-params.test.ts index be2d5e9e68..b58afda8cf 100644 --- a/modules/code-generator/tests/bugfix/i18n-with-params.test.ts +++ b/modules/code-generator/tests/bugfix/i18n-with-params.test.ts @@ -1,7 +1,7 @@ import CodeGenerator from '../../src'; import * as fs from 'fs'; import * as path from 'path'; -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { createDiskPublisher } from '../helpers/solutionHelper'; const testCaseBaseName = path.basename(__filename, '.test.ts'); @@ -19,7 +19,7 @@ describe(testCaseBaseName, () => { ` <Greetings content={this._i18nText({ - key: "greetings.hello", + key: 'greetings.hello', params: { name: this.state.name }, })} /> @@ -31,7 +31,7 @@ describe(testCaseBaseName, () => { function exportProject( importPath: string, outputPath: string, - mergeSchema?: Partial<ProjectSchema>, + mergeSchema?: Partial<IPublicTypeProjectSchema>, ) { const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' }); const schema = { ...JSON.parse(schemaJsonStr), ...mergeSchema }; diff --git a/modules/code-generator/tests/bugfix/icejs-import-wrong-naming.test.ts b/modules/code-generator/tests/bugfix/icejs-import-wrong-naming.test.ts index 51e22b3a97..efb7fd5bee 100644 --- a/modules/code-generator/tests/bugfix/icejs-import-wrong-naming.test.ts +++ b/modules/code-generator/tests/bugfix/icejs-import-wrong-naming.test.ts @@ -1,7 +1,7 @@ import CodeGenerator from '../../src'; import * as fs from 'fs'; import * as path from 'path'; -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { createDiskPublisher } from '../helpers/solutionHelper'; const testCaseBaseName = path.basename(__filename, '.test.ts'); @@ -27,7 +27,7 @@ describe(testCaseBaseName, () => { }); const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); - expect(generatedPageFileContent).toContain(`import Foo from "example-package/lib/index.js";`); + expect(generatedPageFileContent).toContain('import Foo from \'example-package/lib/index.js\';'); }); test('named import with no alias', async () => { @@ -47,7 +47,7 @@ describe(testCaseBaseName, () => { const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); expect(generatedPageFileContent).toContain( - `import { Foo } from "example-package/lib/index.js";`, + 'import { Foo } from \'example-package/lib/index.js\';', ); }); @@ -68,7 +68,7 @@ describe(testCaseBaseName, () => { const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); expect(generatedPageFileContent).toContain( - `import { Bar as Foo } from "example-package/lib/index.js";`, + 'import { Bar as Foo } from \'example-package/lib/index.js\';', ); }); @@ -88,7 +88,7 @@ describe(testCaseBaseName, () => { }); const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); - expect(generatedPageFileContent).toContain(`import Foo from "example-package/lib/index.js";`); + expect(generatedPageFileContent).toContain('import Foo from \'example-package/lib/index.js\';'); }); test('default import with sub name and export name', async () => { @@ -107,9 +107,9 @@ describe(testCaseBaseName, () => { }); const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); - expect(generatedPageFileContent).toContain(`import Bar from "example-package/lib/index.js";`); + expect(generatedPageFileContent).toContain('import Bar from \'example-package/lib/index.js\';'); - expect(generatedPageFileContent).toContain(`const Foo = Bar.Baz;`); + expect(generatedPageFileContent).toContain('const Foo = Bar.Baz;'); }); test('default import with sub name without export name', async () => { @@ -129,10 +129,10 @@ describe(testCaseBaseName, () => { const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); expect(generatedPageFileContent).toContain( - `import __$examplePackage_default from "example-package/lib/index.js";`, + 'import __$examplePackage_default from \'example-package/lib/index.js\';', ); - expect(generatedPageFileContent).toContain(`const Foo = __$examplePackage_default.Baz;`); + expect(generatedPageFileContent).toContain('const Foo = __$examplePackage_default.Baz;'); }); test('named import with sub name', async () => { @@ -152,10 +152,10 @@ describe(testCaseBaseName, () => { const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); expect(generatedPageFileContent).toContain( - `import { Bar } from "example-package/lib/index.js";`, + 'import { Bar } from \'example-package/lib/index.js\';', ); - expect(generatedPageFileContent).toContain(`const Foo = Bar.Baz;`); + expect(generatedPageFileContent).toContain('const Foo = Bar.Baz;'); }); test('default imports with different componentName', async () => { @@ -187,18 +187,18 @@ describe(testCaseBaseName, () => { }); const generatedPageFileContent = readOutputTextFile('demo-project/src/pages/Test/index.jsx'); - expect(generatedPageFileContent).toContain(`import Foo from "example-package";`); - expect(generatedPageFileContent).toContain(`import Baz from "example-package";`); + expect(generatedPageFileContent).toContain('import Foo from \'example-package\';'); + expect(generatedPageFileContent).toContain('import Baz from \'example-package\';'); - expect(generatedPageFileContent).not.toContain(`const Foo =`); - expect(generatedPageFileContent).not.toContain(`const Baz =`); + expect(generatedPageFileContent).not.toContain('const Foo ='); + expect(generatedPageFileContent).not.toContain('const Baz ='); }); }); function exportProject( importPath: string, outputPath: string, - mergeSchema?: Partial<ProjectSchema>, + mergeSchema?: Partial<IPublicTypeProjectSchema>, ) { const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' }); const schema = { ...JSON.parse(schemaJsonStr), ...mergeSchema }; diff --git a/modules/code-generator/tests/bugfix/icejs-missing-imports-1.test.ts b/modules/code-generator/tests/bugfix/icejs-missing-imports-1.test.ts index 17c40a4fa1..cad73474ac 100644 --- a/modules/code-generator/tests/bugfix/icejs-missing-imports-1.test.ts +++ b/modules/code-generator/tests/bugfix/icejs-missing-imports-1.test.ts @@ -22,7 +22,7 @@ test(testCaseBaseName, async () => { Button, Typography, Tag, -} from "@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js";`); +} from '@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js';`); }); function exportProject(inputPath: string, outputPath: string) { diff --git a/modules/code-generator/tests/bugfix/icejs-package-json-dependencies.test.ts b/modules/code-generator/tests/bugfix/icejs-package-json-dependencies.test.ts index 6edd6b9124..88ca02c6ba 100644 --- a/modules/code-generator/tests/bugfix/icejs-package-json-dependencies.test.ts +++ b/modules/code-generator/tests/bugfix/icejs-package-json-dependencies.test.ts @@ -19,15 +19,15 @@ test(testCaseBaseName, async () => { // 里面有的数据源则应该生成对应的 dependencies expect(generatedPackageJson.dependencies).toMatchObject({ - '@alilc/lowcode-datasource-engine': 'latest', - '@alilc/lowcode-datasource-fetch-handler': 'latest', + '@alilc/lowcode-datasource-engine': '^1.0.0', + '@alilc/lowcode-datasource-fetch-handler': '^1.0.0', }); // 里面没有的,则不应该生成对应的 dependencies expect(generatedPackageJson.dependencies).not.toMatchObject({ - '@alilc/lowcode-datasource-url-params-handler': 'latest', - '@alilc/lowcode-datasource-mtop-handler': 'latest', - '@alilc/lowcode-datasource-mopen-handler': 'latest', + '@alilc/lowcode-datasource-url-params-handler': '^1.0.0', + '@alilc/lowcode-datasource-mtop-handler': '^1.0.0', + '@alilc/lowcode-datasource-mopen-handler': '^1.0.0', }); }); diff --git a/modules/code-generator/tests/bugfix/strict-mode-context-1.test.ts b/modules/code-generator/tests/bugfix/strict-mode-context-1.test.ts index 9cfb09325d..ebca6a26fd 100644 --- a/modules/code-generator/tests/bugfix/strict-mode-context-1.test.ts +++ b/modules/code-generator/tests/bugfix/strict-mode-context-1.test.ts @@ -1,7 +1,7 @@ import CodeGenerator from '../../src'; import * as fs from 'fs'; import * as path from 'path'; -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { createDiskPublisher } from '../helpers/solutionHelper'; import { IceJsProjectBuilderOptions } from '../../src/solutions/icejs'; @@ -33,7 +33,7 @@ describe(testCaseBaseName, () => { function exportProject( importPath: string, outputPath: string, - mergeSchema?: Partial<ProjectSchema>, + mergeSchema?: Partial<IPublicTypeProjectSchema>, options?: IceJsProjectBuilderOptions, ) { const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' }); diff --git a/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.schema.json b/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.schema.json index a8e2feb403..347ef7d3e4 100644 --- a/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.schema.json +++ b/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.schema.json @@ -48,10 +48,10 @@ } ], "i18n": { - "zh_CN": { + "zh-CN": { "greetings.hello": "${name}, 你好!" }, - "en_US": { + "en-US": { "greetings.hello": "Hello, ${name}!" } } diff --git a/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.test.ts b/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.test.ts index 11ba9de054..78f2b50b32 100644 --- a/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.test.ts +++ b/modules/code-generator/tests/bugfix/tolerate-eval-errors-1-loop.test.ts @@ -1,7 +1,7 @@ import CodeGenerator from '../../src'; import * as fs from 'fs'; import * as path from 'path'; -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { createDiskPublisher } from '../helpers/solutionHelper'; import { IceJsProjectBuilderOptions } from '../../src/solutions/icejs'; @@ -28,7 +28,7 @@ test('loop should be generated link __$$evalArray(xxx).map', async () => { function exportProject( importPath: string, outputPath: string, - mergeSchema?: Partial<ProjectSchema>, + mergeSchema?: Partial<IPublicTypeProjectSchema>, options?: IceJsProjectBuilderOptions, ) { const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' }); diff --git a/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.schema.json b/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.schema.json index f35a9d16bb..6b35a8db63 100644 --- a/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.schema.json +++ b/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.schema.json @@ -60,10 +60,10 @@ } ], "i18n": { - "zh_CN": { + "zh-CN": { "greetings.hello": "${name}, 你好!" }, - "en_US": { + "en-US": { "greetings.hello": "Hello, ${name}!" } } diff --git a/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.test.ts b/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.test.ts index e8b65a5442..1842005938 100644 --- a/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.test.ts +++ b/modules/code-generator/tests/bugfix/tolerate-eval-errors-2-nested-loop.test.ts @@ -1,7 +1,7 @@ import CodeGenerator from '../../src'; import * as fs from 'fs'; import * as path from 'path'; -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { createDiskPublisher } from '../helpers/solutionHelper'; import { IceJsProjectBuilderOptions } from '../../src/solutions/icejs'; @@ -32,7 +32,7 @@ test('loop should be generated link __$$evalArray(xxx).map', async () => { function exportProject( importPath: string, outputPath: string, - mergeSchema?: Partial<ProjectSchema>, + mergeSchema?: Partial<IPublicTypeProjectSchema>, options?: IceJsProjectBuilderOptions, ) { const schemaJsonStr = fs.readFileSync(importPath, { encoding: 'utf8' }); diff --git a/modules/code-generator/test-cases/.gitignore b/modules/code-generator/tests/fixtures/test-cases/.gitignore similarity index 100% rename from modules/code-generator/test-cases/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/package.json new file mode 100644 index 0000000000..38f24df0b1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/package.json @@ -0,0 +1,44 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.19.18", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..794ad46a48 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,205 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + fetch: __$$createFetchRequestHandler(), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + isInit: function () { + return undefined; + }.bind(_this), + options: function () { + return undefined; + }.bind(_this), + }, + { + id: 'user', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + { + id: 'orders', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data.result; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + dataHandler: function (dataMap) { + console.info('All datasources loaded:', dataMap); + }, + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item label="姓名:" name="name" initValue="李雷"> + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + {__$$evalArray(() => ['a', 'b', 'c']).map((item, index) => + ((__$$context) => + !!__$$eval(() => index >= 1) && ( + <Button type="primary" style={{ margin: '0 5px 0 5px' }}> + {__$$eval(() => item)} + </Button> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/schema.json5 new file mode 100644 index 0000000000..76c52fb5e8 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo1/schema.json5 @@ -0,0 +1,276 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button" + }, + { + "componentName": "Button.Group", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "Input", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Input" + }, + { + "componentName": "Form", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form" + }, + { + "componentName": "Form.Item", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form", + "subName": "Item" + }, + { + "componentName": "NumberPicker", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "NumberPicker" + }, + { + "componentName": "Select", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Select" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node$1", + "meta": { + "title": "测试", + "router": "/" + }, + "props": { + "ref": "outterView", + "autoLoading": true + }, + "fileName": "test", + "state": { + "text": "outter" + }, + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() { console.log('componentDidMount'); }" + } + }, + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/user + { + id: 'user', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', + }, + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders + { + id: 'orders', + type: 'fetch', + options: { + method: 'GET', + uri: "https://shs.xxx.com/mock/1458/demo/orders", + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', + }, + }, + ], + dataHandler: { + type: 'JSExpression', + value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', + }, + }, + "children": [ + { + "componentName": "Form", + "id": "node$2", + "props": { + "labelCol": { + "type": "JSExpression", + "value": "this.state.colNum" + }, + "style": {}, + "ref": "testForm" + }, + "children": [ + { + "componentName": "Form.Item", + "id": "node$3", + "props": { + "label": "姓名:", + "name": "name", + "initValue": "李雷" + }, + "children": [ + { + "componentName": "Input", + "id": "node$4", + "props": { + "placeholder": "请输入", + "size": "medium", + "style": { + "width": 320 + } + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$5", + "props": { + "label": "年龄:", + "name": "age", + "initValue": "22" + }, + "children": [ + { + "componentName": "NumberPicker", + "id": "node$6", + "props": { + "size": "medium", + "type": "normal" + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$7", + "props": { + "label": "职业:", + "name": "profession" + }, + "children": [ + { + "componentName": "Select", + "id": "node$8", + "props": { + "dataSource": [ + { + "label": "教师", + "value": "t" + }, + { + "label": "医生", + "value": "d" + }, + { + "label": "歌手", + "value": "s" + } + ] + } + } + ] + }, + { + "componentName": "Div", + "id": "node$9", + "props": { + "style": { + "textAlign": "center" + } + }, + "children": [ + { + "componentName": "Button.Group", + "id": "node$a", + "props": {}, + "children": [ + { + "componentName": "Button", + "id": "node$b", + "condition": { + "type": "JSExpression", + "value": "this.index >= 1" + }, + "loop": ["a", "b", "c"], + "props": { + "type": "primary", + "style": { + "margin": "0 5px 0 5px" + }, + }, + "children": [ + { + "type": "JSExpression", + "value": "this.item" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.xxx.com" + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + } + }, + "theme": { + "package": "@alife/theme-fusion", + "version": "^0.1.0", + "primary": "#ff9966" + } + }, + "meta": { + "name": "demo应用", + "git_group": "appGroup", + "project_name": "app_demo", + "description": "这是一个测试应用", + "spma": "spa23d", + "creator": "月飞" + } +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/package.json new file mode 100644 index 0000000000..3a0015c9bd --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/package.json @@ -0,0 +1,48 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/b6-page": "^0.1.0", + "@alilc/b6-text": "^0.1.0", + "@alilc/b6-compact-legao-builtin": "1.x", + "antd": "3.x", + "@alilc/b6-util-mocks": "1.x" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx new file mode 100644 index 0000000000..2945a9d8f5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx @@ -0,0 +1,118 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Page } from '@alilc/b6-page'; + +import { Text } from '@alilc/b6-text'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Aaaa$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + description: 'URL参数', + options: function () { + return { + uri: '', + }; + }.bind(_this), + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div title="" backgroundColor="#fff" textColor="#333" style={{}}> + <Text + content="欢迎使用 BuildSuccess!sadsad" + style={{}} + fieldId="text_kp6ci11t" + /> + </div> + ); + } +} + +export default Aaaa$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..868d471106 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/expected/demo-project/src/utils.js @@ -0,0 +1,61 @@ +import legaoBuiltin from '@alilc/b6-compact-legao-builtin'; + +import { message, Modal as modal } from 'antd'; + +import { mocks } from '@alilc/b6-util-mocks'; + +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default { + legaoBuiltin, + + message, + + mocks, + + modal, +}; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2-utils-name-alias/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/package.json new file mode 100644 index 0000000000..8f02ff1c7e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/package.json @@ -0,0 +1,42 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.19.18", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..e8cb58e640 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/i18n.js @@ -0,0 +1,86 @@ +const i18nConfig = { + 'zh-CN': { + 'i18n-jwg27yo4': '你好', + 'i18n-jwg27yo3': '中国', + }, + 'en-US': { + 'i18n-jwg27yo4': 'Hello', + 'i18n-jwg27yo3': 'China', + }, +}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..080c4245b6 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,129 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidMount() { + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item + label={__$$eval(() => this.i18n('i18n-jwg27yo4'))} + name="name" + initValue="李雷" + > + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + <Button + type="primary" + style={{ margin: '0 5px 0 5px' }} + htmlType="submit" + > + 提交 + </Button> + <Button + type="normal" + style={{ margin: '0 5px 0 5px' }} + htmlType="reset" + > + 重置 + </Button> + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/schema.json5 new file mode 100644 index 0000000000..2228212067 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo2/schema.json5 @@ -0,0 +1,256 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button" + }, + { + "componentName": "Button.Group", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "Input", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Input" + }, + { + "componentName": "Form", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form" + }, + { + "componentName": "Form.Item", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form", + "subName": "Item" + }, + { + "componentName": "NumberPicker", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "NumberPicker" + }, + { + "componentName": "Select", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Select" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node$1", + "meta": { + "title": "测试", + "router": "/" + }, + "props": { + "ref": "outterView", + "autoLoading": true + }, + "fileName": "test", + "state": { + "text": "outter" + }, + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() { console.log('componentDidMount'); }" + } + }, + "children": [ + { + "componentName": "Form", + "id": "node$2", + "props": { + "labelCol": { + "type": "JSExpression", + "value": "this.state.colNum" + }, + "style": {}, + "ref": "testForm" + }, + "children": [ + { + "componentName": "Form.Item", + "id": "node$3", + "props": { + "label": { + type: 'JSExpression', + value: 'this.i18n("i18n-jwg27yo4")', + }, + "name": "name", + "initValue": "李雷" + }, + "children": [ + { + "componentName": "Input", + "id": "node$4", + "props": { + "placeholder": "请输入", + "size": "medium", + "style": { + "width": 320 + } + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$5", + "props": { + "label": "年龄:", + "name": "age", + "initValue": "22" + }, + "children": [ + { + "componentName": "NumberPicker", + "id": "node$6", + "props": { + "size": "medium", + "type": "normal" + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$7", + "props": { + "label": "职业:", + "name": "profession" + }, + "children": [ + { + "componentName": "Select", + "id": "node$8", + "props": { + "dataSource": [ + { + "label": "教师", + "value": "t" + }, + { + "label": "医生", + "value": "d" + }, + { + "label": "歌手", + "value": "s" + } + ] + } + } + ] + }, + { + "componentName": "Div", + "id": "node$9", + "props": { + "style": { + "textAlign": "center" + } + }, + "children": [ + { + "componentName": "Button.Group", + "id": "node$a", + "props": {}, + "children": [ + { + "componentName": "Button", + "id": "node$b", + "props": { + "type": "primary", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "submit" + }, + "children": [ + "提交" + ] + }, + { + "componentName": "Button", + "id": "node$d", + "props": { + "type": "normal", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "reset" + }, + "children": [ + "重置" + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.xxx.com" + }, + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "中国" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "China" + } + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + } + }, + "theme": { + "package": "@alife/theme-fusion", + "version": "^0.1.0", + "primary": "#ff9966" + } + }, + "meta": { + "name": "demo应用", + "git_group": "appGroup", + "project_name": "app_demo", + "description": "这是一个测试应用", + "spma": "spa23d", + "creator": "月飞" + } +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/package.json new file mode 100644 index 0000000000..8f02ff1c7e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/package.json @@ -0,0 +1,42 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.19.18", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..e8cb58e640 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/i18n.js @@ -0,0 +1,86 @@ +const i18nConfig = { + 'zh-CN': { + 'i18n-jwg27yo4': '你好', + 'i18n-jwg27yo3': '中国', + }, + 'en-US': { + 'i18n-jwg27yo4': 'Hello', + 'i18n-jwg27yo3': 'China', + }, +}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..2f0a6efa9a --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,107 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import Super, { + Button, + Input as CustomInput, + Form, + NumberPicker, + Select, + SearchTable as SearchTableExport, +} from '@alifd/next'; + +import SuperOther from '@alifd/next'; + +import '@alifd/next/lib/super/style'; + +import '@alifd/next/lib/button/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/search-table/style'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const SuperSub = Super.Sub; + +const SelectOption = Select.Option; + +const SearchTable = SearchTableExport.default; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + <Super title={__$$eval(() => this.state.title)} /> + <SuperSub /> + <SuperOther /> + <Button /> + <Button.Group /> + <CustomInput /> + <Form.Item /> + <NumberPicker /> + <SelectOption /> + <SearchTable /> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo3/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo3/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/package.json new file mode 100644 index 0000000000..393be4663e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/package.json @@ -0,0 +1,45 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0", + "@alife/container": "^1.0.0", + "@alife/mc-assets-1935": "0.1.9" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..6400d7445b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,292 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, + Text as NextText, +} from '@alife/container/lib/index.js'; + +import { AliSearchTable as AliSearchTableExport } from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +const AliSearchTable = AliSearchTableExport.default; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { fetch: __$$createFetchRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter', isShowDialog: false }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + type: 'fetch', + isInit: function () { + return true; + }.bind(_this), + options: function () { + return { + params: {}, + method: 'GET', + isCors: true, + timeout: 5000, + headers: {}, + uri: 'https://mocks.xxx.com/mock/jjpin/user/list', + }; + }.bind(_this), + id: 'users', + }, + ], + }; + } + + componentWillUnmount() { + console.log('will umount'); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + console.log(this.state); + } + + testFunc() { + console.log('test func'); + } + + onClick() { + this.setState({ + isShowDialog: true, + }); + } + + closeDialog() { + this.setState({ + isShowDialog: false, + }); + } + + onSearch(values) { + console.log('search form:', values); + console.log(this.dataSourceMap); + this.dataSourceMap['users'].load(values); + } + + onClear() { + console.log('form reset'); + this.setState({ + isShowDialog: true, + }); + } + + onPageChange(page, pageSize) { + console.log(`page: ${page}, pageSize: ${pageSize}`); + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('did mount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <NextPage + columns={12} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={ + <NextP + wrap={true} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <NextText type="h5">员工列表</NextText> + </NextP> + } + headerTest={[]} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + colSpan={12} + rowSpan={1} + childTotalColumns="1fr" + > + <NextBlockCell + title="" + primaryKey="732" + prefix="next-" + placeholderStyle={{ height: '100%' }} + colSpan={1} + rowSpan={1} + > + <NextP + wrap={true} + type="body2" + textSpacing={true} + verAlign="center" + align="flex-start" + flex={true} + > + <AliSearchTable + dataSource={__$$eval(() => this.state.users.data)} + rowKey="workid" + columns={[ + { title: '花名', dataIndex: 'cname' }, + { title: 'user_id', dataIndex: 'workid' }, + { title: '部门', dataIndex: 'dep' }, + ]} + searchItems={[ + { label: '姓名', name: 'cname' }, + { label: '部门', name: 'dep' }, + ]} + onSearch={function () { + return this.onSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onClear={function () { + return this.onClear.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + pagination={{ + defaultPageSize: '', + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + showSizeChanger: true, + }} + /> + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + colSpan={12} + rowSpan={1} + childTotalColumns={1} + > + <NextBlockCell + title="" + primaryKey="472" + prefix="next-" + placeholderStyle={{ height: '100%' }} + colSpan={1} + rowSpan={1} + /> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/schema.json5 new file mode 100644 index 0000000000..ca97204e9c --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo4/schema.json5 @@ -0,0 +1,353 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "package": "@alife/mc-assets-1935", + "version": "0.1.9", + "exportName": "AliSearchTable", + "main": "build/lowcode/index.js", + "subName": "default", + "destructuring": true, + "componentName": "AliSearchTable" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "P", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextP" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Block", + "main": "lib/index.js", + "destructuring": true, + "subName": "Cell", + "componentName": "NextBlockCell" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Block", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextBlock" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Text", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextText" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Page", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextPage" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node_dockcviv8fo1", + "props": { + "ref": "outterView", + "style": { + "height": "100%" + } + }, + "fileName": "test", + "dataSource": { + "list": [ + { + "type": "fetch", + "isInit": true, + "options": { + "params": {}, + "method": "GET", + "isCors": true, + "timeout": 5000, + "headers": {}, + "uri": "https://mocks.xxx.com/mock/jjpin/user/list" + }, + "id": "users" + } + ] + }, + "css": "body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}", + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() {\n console.log('did mount');\n }" + }, + "componentWillUnmount": { + "type": "JSFunction", + "value": "function() {\n console.log('will umount');\n }" + }, + "componentDidUpdate": { + "type": "JSFunction", + "value": "function(prevProps, prevState, snapshot) {\n console.log(this.state);\n }" + } + }, + "methods": { + "testFunc": { + "type": "JSFunction", + "value": "function() {\n console.log('test func');\n }" + }, + "onClick": { + "type": "JSFunction", + "value": "function() {\n this.setState({\n isShowDialog: true\n })\n }" + }, + "closeDialog": { + "type": "JSFunction", + "value": "function() {\n this.setState({\n isShowDialog: false\n })\n }" + }, + "onSearch": { + "type": "JSFunction", + "value": "function(values) {\n console.log('search form:', values)\n console.log(this.dataSourceMap);\n this.dataSourceMap['users'].load(values)\n }" + }, + "onClear": { + "type": "JSFunction", + "value": "function() {\n console.log('form reset')\n this.setState({\n isShowDialog: true\n })\n }" + }, + "onPageChange": { + "type": "JSFunction", + "value": "function(page, pageSize) {\n console.log(`page: ${page}, pageSize: ${pageSize}`)\n }" + } + }, + "state": { + "text": "outter", + "isShowDialog": false + }, + "children": [ + { + "componentName": "NextPage", + "id": "node_ockkgjwi8z1", + "props": { + "columns": 12, + "placeholderStyle": { + "gridRowEnd": "span 1", + "gridColumnEnd": "span 12" + }, + "placeholder": "页面主体内容:拖拽Block布局组件到这里", + "header": { + "type": "JSSlot", + "value": [ + { + "componentName": "NextP", + "id": "node_ockkgjwi8zn", + "props": { + "wrap": true, + "type": "body2", + "verAlign": "middle", + "textSpacing": true, + "align": "left", + "flex": true + }, + "children": [ + { + "componentName": "NextText", + "id": "node_ockkgjwi8zo", + "props": { + "type": "h5", + "children": "员工列表" + } + } + ] + } + ], + "title": "header" + }, + "headerTest": { + "type": "JSSlot", + "value": [], + "title": "header" + }, + "headerProps": { + "background": "surface" + }, + "footer": { + "type": "JSSlot", + "title": "footer" + }, + "minHeight": "100vh" + }, + "children": [ + { + "componentName": "NextBlock", + "id": "node_ockkgjwi8z2", + "props": { + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "noPadding": false, + "noBorder": false, + "background": "surface", + "colSpan": 12, + "rowSpan": 1, + "childTotalColumns": "1fr" + }, + "title": "分区", + "children": [ + { + "componentName": "NextBlockCell", + "id": "node_ockkgjwi8z3", + "props": { + "title": "", + "primaryKey": "732", + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "colSpan": 1, + "rowSpan": 1 + }, + "children": [ + { + "componentName": "NextP", + "id": "node_ockkgjwi8zu", + "props": { + "wrap": true, + "type": "body2", + "textSpacing": true, + "verAlign": "center", + "align": "flex-start", + "flex": true + }, + "children": [ + { + "componentName": "AliSearchTable", + "id": "node_ockkgjwi8zv", + "props": { + "dataSource": { + "type": "JSExpression", + "value": "this.state.users.data" + }, + "rowKey": "workid", + "columns": [ + { + "title": "花名", + "dataIndex": "cname" + }, + { + "title": "user_id", + "dataIndex": "workid" + }, + { + "title": "部门", + "dataIndex": "dep" + } + ], + "searchItems": [ + { + "label": "姓名", + "name": "cname" + }, + { + "label": "部门", + "name": "dep" + } + ], + "onSearch": { + "type": "JSFunction", + "value": "function(){ return this.onSearch.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "onClear": { + "type": "JSFunction", + "value": "function(){ return this.onClear.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "pagination": { + "defaultPageSize": "", + "onPageChange": { + "type": "JSFunction", + "value": "function(){ return this.onPageChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "showSizeChanger": true + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "componentName": "NextPage", + "id": "node_ockm4jxd6313", + "props": { + "columns": 12, + "headerDivider": true, + "placeholderStyle": { + "gridRowEnd": "span 1", + "gridColumnEnd": "span 12" + }, + "placeholder": "页面主体内容:拖拽Block布局组件到这里", + "header": { + "type": "JSSlot", + "title": "header" + }, + "headerProps": { + "background": "surface" + }, + "footer": { + "type": "JSSlot", + "title": "footer" + }, + "minHeight": "100vh" + }, + "title": "页面", + "children": [ + { + "componentName": "NextBlock", + "id": "node_ockm4jxd6314", + "props": { + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "noPadding": false, + "noBorder": false, + "background": "surface", + "colSpan": 12, + "rowSpan": 1, + "childTotalColumns": 1 + }, + "title": "区块", + "children": [ + { + "componentName": "NextBlockCell", + "id": "node_ockm4jxd6315", + "props": { + "title": "", + "primaryKey": "472", + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "colSpan": 1, + "rowSpan": 1 + } + } + ] + } + ] + } + ] + } + ], + "i18n": {} +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/package.json new file mode 100644 index 0000000000..d007025a60 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/package.json @@ -0,0 +1,46 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alife/container": "0.3.7", + "@alilc/antd-lowcode": "0.5.4", + "@alife/mc-assets-1935": "0.1.16" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..7427f164d9 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,389 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import { + Card, + Space, + Typography, + Select, + Button, + Modal, + Form, + InputNumber, + Input, +} from '@alilc/antd-lowcode/dist/antd-lowcode.esm.js'; + +import { AliAutoSearchTable } from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + name: 'nongzhou', + gateways: [], + selectedGateway: null, + records: [], + modalVisible: false, + }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentWillUnmount() { + /* ... */ + } + + componentDidUpdate() { + /* ... */ + } + + onChange() { + /* ... */ + } + + getActions() { + /* ... */ + } + + onCreateOrder() { + /* ... */ + } + + onCancelModal() { + /* ... */ + } + + onConfirmCreateOrder() { + /* ... */ + } + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + style={{ cursor: 'pointer' }} + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + > + <Card title=""> + <Space size={0} align="center" direction="horizontal"> + <Typography.Text>所在网关:</Typography.Text> + <Select + style={{ + marginTop: '16px', + marginRight: '16px', + marginBottom: '16px', + marginLeft: '16px', + width: '400px', + display: 'inline-block', + }} + options={__$$eval(() => this.state.gateways)} + mode="single" + defaultValue={['auto-edd-uniproxy']} + labelInValue={true} + showSearch={true} + allowClear={false} + placeholder="请选取网关" + showArrow={true} + loading={false} + tokenSeparators={[]} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'onChange', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: false }, + { name: 'onSelect', disabled: false }, + { name: 'onDropdownVisibleChange', disabled: false }, + ], + }} + onChange={function () { + this.onChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Space> + <Button + type="primary" + style={{ + display: 'block', + marginTop: '20px', + marginBottom: '20px', + }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCreateOrder', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onCreateOrder.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 创建发布单 + </Button> + <Modal + title="创建发布单" + visible={__$$eval(() => this.state.modalVisible)} + footer="" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onCancelModal.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + zIndex={2000} + > + <Form + labelCol={{ span: 6 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onConfirmCreateOrder.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onConfirmCreateOrder', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label="发布批次"> + <InputNumber value={3} min={1} /> + </Form.Item> + <Form.Item label="批次间隔时间"> + <InputNumber value={3} /> + </Form.Item> + <Form.Item label="备注 "> + <Input.TextArea rows={3} placeholder="请输入" /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: 6 }} + style={{ + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + display: 'flex', + }} + labelAlign="right" + > + <Button type="primary" htmlType="submit"> + 提交 + </Button> + <Button + style={{ marginLeft: 20 }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onCancelModal.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 取消 + </Button> + </Form.Item> + </Form> + </Modal> + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.records)} + columns={[ + { + title: '发布名称', + dataIndex: 'order_name', + key: 'name', + }, + { + title: '类型', + dataIndex: 'order_type_desc', + key: 'age', + }, + { + title: '发布状态', + dataIndex: 'order_status_desc', + key: 'address', + }, + { title: '发布人', dataIndex: 'creator_name' }, + { title: '当前批次/总批次', dataIndex: 'cur_batch_no' }, + { + title: '发布机器/总机器', + dataIndex: 'pubblish_ip_finish_num', + }, + { title: '发布时间', dataIndex: 'publish_id' }, + ]} + actions={__$$eval(() => this.actions || [])} + getActions={function () { + return this.getActions.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Card> + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo5/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo5/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/package.json new file mode 100644 index 0000000000..38f24df0b1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/package.json @@ -0,0 +1,44 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.19.18", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..515940c334 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,205 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + fetch: __$$createFetchRequestHandler(), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + isInit: function () { + return undefined; + }.bind(_this), + options: function () { + return undefined; + }.bind(_this), + }, + { + id: 'user', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + { + id: 'orders', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data.result; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + dataHandler: function (dataMap) { + console.info('All datasources loaded:', dataMap); + }, + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item label="姓名:" name="name" initValue="李雷"> + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + {__$$evalArray(() => ['a', 'b', 'c']).map((item, index) => + ((__$$context) => + !!false && ( + <Button type="primary" style={{ margin: '0 5px 0 5px' }}> + {__$$eval(() => item)} + </Button> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/schema.json5 new file mode 100644 index 0000000000..5b6776c1ee --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo6-literal-condition/schema.json5 @@ -0,0 +1,273 @@ +{ + version: '1.0.0', + componentsMap: [ + { + componentName: 'Button', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Button', + }, + { + componentName: 'Button.Group', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Button', + subName: 'Group', + }, + { + componentName: 'Input', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Input', + }, + { + componentName: 'Form', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Form', + }, + { + componentName: 'Form.Item', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Form', + subName: 'Item', + }, + { + componentName: 'NumberPicker', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'NumberPicker', + }, + { + componentName: 'Select', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Select', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node$1', + meta: { + title: '测试', + router: '/', + }, + props: { + ref: 'outterView', + autoLoading: true, + }, + fileName: 'test', + state: { + text: 'outter', + }, + lifeCycles: { + componentDidMount: { + type: 'JSFunction', + value: "function() { console.log('componentDidMount'); }", + }, + }, + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/user + { + id: 'user', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }, + dataHandler: { + type: 'JSFunction', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', + }, + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders + { + id: 'orders', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }, + dataHandler: { + type: 'JSFunction', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', + }, + }, + ], + dataHandler: { + type: 'JSFunction', + value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', + }, + }, + children: [ + { + componentName: 'Form', + id: 'node$2', + props: { + labelCol: { + type: 'JSExpression', + value: 'this.state.colNum', + }, + style: {}, + ref: 'testForm', + }, + children: [ + { + componentName: 'Form.Item', + id: 'node$3', + props: { + label: '姓名:', + name: 'name', + initValue: '李雷', + }, + children: [ + { + componentName: 'Input', + id: 'node$4', + props: { + placeholder: '请输入', + size: 'medium', + style: { + width: 320, + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node$5', + props: { + label: '年龄:', + name: 'age', + initValue: '22', + }, + children: [ + { + componentName: 'NumberPicker', + id: 'node$6', + props: { + size: 'medium', + type: 'normal', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node$7', + props: { + label: '职业:', + name: 'profession', + }, + children: [ + { + componentName: 'Select', + id: 'node$8', + props: { + dataSource: [ + { + label: '教师', + value: 't', + }, + { + label: '医生', + value: 'd', + }, + { + label: '歌手', + value: 's', + }, + ], + }, + }, + ], + }, + { + componentName: 'Div', + id: 'node$9', + props: { + style: { + textAlign: 'center', + }, + }, + children: [ + { + componentName: 'Button.Group', + id: 'node$a', + props: {}, + children: [ + { + componentName: 'Button', + id: 'node$b', + condition: false, + loop: ['a', 'b', 'c'], + props: { + type: 'primary', + style: { + margin: '0 5px 0 5px', + }, + }, + children: [ + { + type: 'JSExpression', + value: 'this.item', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + constants: { + ENV: 'prod', + DOMAIN: 'xxx.xxx.com', + }, + css: 'body {font-size: 12px;} .table { width: 100px;}', + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'J_Container', + layout: { + componentName: 'BasicLayout', + props: { + logo: '...', + name: '测试网站', + }, + }, + theme: { + package: '@alife/theme-fusion', + version: '^0.1.0', + primary: '#ff9966', + }, + }, + meta: { + name: 'demo应用', + git_group: 'appGroup', + project_name: 'app_demo', + description: '这是一个测试应用', + spma: 'spa23d', + creator: '月飞', + }, +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/package.json new file mode 100644 index 0000000000..fcdfc7bd4b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/package.json @@ -0,0 +1,45 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode": "0.8.0", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..9e93a3ff6f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,1076 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Steps, + Form, + Input, + Checkbox, + Select, + DatePicker, + InputNumber, + Button, +} from '@alilc/antd-lowcode/dist/antd-lowcode.esm.js'; + +import { + Text as NextText, + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + books: [], + currentStep: 0, + isModifyDialogVisible: false, + isModifyStatus: false, + secondCommitText: '完成并提交', + thirdAuditText: '审核中', + thirdButtonText: '修改', + customerProjectInfo: { + id: null, + systemProjectName: null, + projectVersionTypeArray: null, + projectVersionType: null, + versionLine: 2, + expectedTime: null, + expectedNum: null, + projectModal: null, + displayWidth: null, + displayHeight: null, + displayInch: null, + displayDpi: null, + mainSoc: null, + cpuCoreNum: null, + instructions: null, + osVersion: null, + status: null, + }, + versionLinesArray: [ + { label: 'AmapAuto 485', value: 1 }, + { label: 'AmapAuto 505', value: 2 }, + ], + projectModalsArray: [ + { label: '车机', value: 1 }, + { label: '车镜', value: 2 }, + { label: '记录仪', value: 3 }, + { label: '其他', value: 4 }, + ], + osVersionsArray: [ + { label: '安卓5', value: 1 }, + { label: '安卓6', value: 2 }, + { label: '安卓7', value: 3 }, + { label: '安卓8', value: 4 }, + { label: '安卓9', value: 5 }, + { label: '安卓10', value: 6 }, + ], + instructionsArray: [ + { label: 'ARM64-V8', value: 'ARM64-V8' }, + { label: 'ARM32-V7', value: 'ARM32-V7' }, + { label: 'X86', value: 'X86' }, + { label: 'X64', value: 'X64' }, + ], + }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + /*...*/ + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initUtils() { + /*...*/ + } + + onFinishFirst() { + /*...*/ + } + + onClickPreSecond() { + /*...*/ + } + + onFinishSecond() { + /*...*/ + } + + onClickModifyThird() { + /*...*/ + } + + onOkModifyDialogThird() { + //第三步 修改 对话框 确定 + + this.setState({ + currentStep: 0, + isModifyDialogVisible: false, + }); + } + + onCancelModifyDialogThird() { + //第三步 修改 对话框 取消 + + this.setState({ + isModifyDialogVisible: false, + }); + } + + onFinishFailed() {} + + onClickPreThird() { + // 第三步 上一步 + this.setState({ + currentStep: 1, + }); + } + + onClickFirstBack() { + // 第一步 返回按钮 + this.$router.push('/myProjectList'); + } + + onClickSecondBack() { + // 第二步 返回按钮 + this.$router.push('/myProjectList'); + } + + onClickThirdBack() { + // 第三步 返回按钮 + this.$router.push('/myProjectList'); + } + + onValuesChange(_, values) { + this.setState({ + customerProjectInfo: { + ...this.state.customerProjectInfo, + ...values, + }, + }); + } + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="是否修改" + visible={__$$eval(() => this.state.isModifyDialogVisible)} + okText="确认" + okType="" + forceRender={false} + cancelText="取消" + zIndex={2000} + destroyOnClose={false} + confirmLoading={false} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onOk', + relatedEventName: 'onOkModifyDialogThird', + }, + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModifyDialogThird', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: true }, + ], + }} + onOk={function () { + this.onOkModifyDialogThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onCancel={function () { + this.onCancelModifyDialogThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + <NextText + type="inherit" + style={{ + fontStyle: 'normal', + textAlign: 'left', + display: 'block', + fontFamily: 'arial, helvetica, microsoft yahei', + fontWeight: 'normal', + }} + > + 修改将撤回此前填写的信息 + </NextText> + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + style={{}} + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + style={{ marginBottom: '24px' }} + > + <Steps current={__$$eval(() => this.state.currentStep)}> + <Steps.Step title="版本申请" description="" /> + <Steps.Step title="机器配置" subTitle="" description="" /> + <Steps.Step title="项目审批" description="" /> + </Steps> + </NextP> + {!!__$$eval(() => this.state.currentStep === 0) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinish={function () { + this.onFinishFirst.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishFirst', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: true }, + ], + }} + initialValues={__$$eval( + () => this.state.customerProjectInfo + )} + onValuesChange={function () { + this.onValuesChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + {!!false && ( + <Form.Item + label="" + style={{ width: '600px' }} + colon={false} + name="id" + > + <Input + placeholder="" + style={{ width: '600px' }} + bordered={false} + disabled={true} + /> + </Form.Item> + )} + <Form.Item + label="版本类型选择" + name="projectVersionTypeArray" + initialValue="" + labelAlign="left" + colon={false} + required={true} + style={{ flexDirection: 'column', width: '600px' }} + requiredobj={{ + required: true, + message: '请选择版本类型', + }} + > + <Checkbox.Group + options={[ + { label: '基础版本', value: '3' }, + { label: 'AR导航', value: '1' }, + { label: '货车导航', value: '2' }, + { label: 'UI定制', value: '4', disabled: false }, + ]} + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="版本线选择" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="versionLine" + requiredobj={{ required: true, message: '请选择版本线' }} + extra="" + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.versionLinesArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择版本线" + /> + </Form.Item> + <Form.Item + label="项目名称" + colon={false} + required={true} + style={{ display: 'flex' }} + labelAlign="left" + extra="" + name="systemProjectName" + requiredobj={{ + required: true, + message: '请按格式填写项目名称', + }} + typeobj={{ + type: 'string', + message: + '请输入项目名称,格式:公司简称-产品名称-版本类型', + }} + lenobj={{ + max: 100, + message: '项目名称不能超过100个字符', + }} + > + <Input + placeholder="公司简称-产品名称-版本类型" + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="预期交付时间" + style={{ width: '600px' }} + colon={false} + required={true} + name="expectedTime" + labelAlign="left" + requiredobj={{ + required: true, + message: '请填写预期交付时间', + }} + > + <DatePicker + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="预期出货量" + style={{ width: '600px' }} + required={true} + requiredobj={{ + required: true, + message: '请填写预期出货量', + }} + name="expectedNum" + labelAlign="left" + colon={false} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="单位(台)使用该版本的机器数量+预计出货量,请如实填写" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + size="middle" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + alignItems: 'baseline', + justifyContent: 'space-between', + width: '600px', + display: 'block', + }} + labelAlign="left" + colon={false} + > + <Button + style={{ margin: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickFirstBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickFirstBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ + boxShadow: 'rgba(31, 56, 88, 0.2) 0px 0px 0px 0px', + float: 'right', + }} + __events={{ + eventDataList: [], + eventList: [{ name: 'onClick', disabled: false }], + }} + > + 下一步 + </Button> + </Form.Item> + </Form> + </NextP> + )} + {!!__$$eval(() => this.state.currentStep === 1) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinish={function () { + this.onFinishSecond.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + height: '800px', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishSecond', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: true }, + ], + }} + initialValues={__$$eval( + () => this.state.customerProjectInfo + )} + onValuesChange={function () { + this.onValuesChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + <Form.Item + label="设备类型选择" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="projectModal" + requiredobj={{ + required: true, + message: '请选择设备类型', + }} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.projectModalsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择设备类型" + /> + </Form.Item> + <Form.Item + label="屏幕分辨率宽" + style={{ width: '600px' }} + name="displayWidth" + colon={false} + required={true} + requiredobj={{ + required: true, + message: '请输入屏幕分辨率宽', + }} + labelAlign="left" + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="例如1280" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕分辨率高" + style={{ width: '600px' }} + labelAlign="left" + colon={false} + name="displayHeight" + required={true} + requiredobj={{ + required: true, + message: '请输入屏幕分辨率高', + }} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="例如720" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕尺寸(inch)" + style={{ width: '600px' }} + name="displayInch" + labelAlign="left" + required={true} + colon={false} + requiredobj={{ + required: true, + message: '请输入屏幕尺寸', + }} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="请输入尺寸" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕DPI" + style={{ width: '600px' }} + labelAlign="left" + colon={false} + required={false} + name="displayDpi" + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="UI定制项目必填" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="芯片名称" + colon={false} + required={true} + style={{ display: 'flex' }} + labelAlign="left" + extra="" + name="mainSoc" + requiredobj={{ + required: true, + message: '请输入芯片名称', + }} + lenobj={{ max: 50, message: '芯片名称不能超过50个字符' }} + > + <Input + placeholder="请输入芯片名称" + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="芯片核数" + style={{ width: '600px' }} + required={true} + requiredobj={{ + required: true, + message: '请输入芯片核数', + }} + name="cpuCoreNum" + labelAlign="left" + colon={false} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="请输入芯片核数" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + defaultValue="" + min={0} + /> + </Form.Item> + <Form.Item + label="指令集" + style={{ width: '600px' }} + required={true} + requiredobj={{ required: true, message: '请选择指令集' }} + name="instructions" + colon={false} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.instructionsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="系统版本" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="osVersion" + requiredobj={{ + required: true, + message: '请选择系统版本', + }} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.osVersionsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择系统版本" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + width: '600px', + display: 'flex', + }} + > + <Button + style={{ marginLeft: '0' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickSecondBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickSecondBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ float: 'right', marginLeft: '20px' }} + loading={__$$eval( + () => + this.state.LOADING_ADD_OR_UPDATE_CUSTOMER_PROJECT + )} + > + {__$$eval(() => this.state.secondCommitText)} + </Button> + <Button + type="primary" + htmlType="submit" + style={{ marginLeft: '0px', float: 'right' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreSecond', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickPreSecond.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 上一步 + </Button> + </Form.Item> + </Form> + </NextP> + )} + {!!__$$eval(() => this.state.currentStep === 2) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinishFailed={function () { + this.onFinishFailed.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinishFailed', + relatedEventName: 'onFinishFailed', + }, + ], + eventList: [ + { name: 'onFinish', disabled: false }, + { name: 'onFinishFailed', disabled: true }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label=""> + <Steps + current={1} + style={{ + width: '600px', + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + height: '300px', + }} + labelPlacement="horizontal" + direction="vertical" + > + <Steps.Step + title="提交完成" + description="" + style={{ width: '200px' }} + /> + <Steps.Step + title={__$$eval(() => this.state.thirdAuditText)} + subTitle="" + description="" + style={{ width: '200px' }} + /> + </Steps> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + width: '600px', + display: 'flex', + }} + > + <Button + style={{ marginLeft: '0' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickThirdBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickThirdBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ float: 'right', marginLeft: '20px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickModifyThird', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickModifyThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + {__$$eval(() => this.state.thirdButtonText)} + </Button> + {!!__$$eval( + () => this.state.customerProjectInfo.status > 2 + ) && ( + <Button + type="primary" + htmlType="submit" + style={{ marginLeft: '0px', float: 'right' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreThird', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickPreThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 上一步 + </Button> + )} + </Form.Item> + </Form> + </NextP> + )} + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo7-literal-condition2/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/package.json new file mode 100644 index 0000000000..342a5a0774 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/package.json @@ -0,0 +1,44 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-http-handler": "^1.0.0", + "@alilc/lowcode-components": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx new file mode 100644 index 0000000000..9a661ad753 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx @@ -0,0 +1,116 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Page, Table } from '@alilc/lowcode-components'; + +import { createHttpHandler as __$$createHttpRequestHandler } from '@alilc/lowcode-datasource-http-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Example$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { http: __$$createHttpRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'userList', + type: 'http', + description: '用户列表', + options: function () { + return { + uri: 'https://api.example.com/user/list', + }; + }.bind(_this), + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + <Table + dataSource={__$$eval(() => this.dataSourceMap['userList'])} + columns={[ + { dataIndex: 'name', title: '姓名' }, + { dataIndex: 'age', title: '年龄' }, + ]} + /> + </div> + ); + } +} + +export default Example$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo8-datasource-prop/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/package.json new file mode 100644 index 0000000000..d7d07dc6ed --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/package.json @@ -0,0 +1,43 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.19.18", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-jsonp-handler": "^1.0.0" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx new file mode 100644 index 0000000000..799ca0d28f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx @@ -0,0 +1,125 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Switch } from '@alifd/next'; + +import { createJsonpHandler as __$$createJsonpRequestHandler } from '@alilc/lowcode-datasource-jsonp-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/switch/style'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class $$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { jsonp: __$$createJsonpRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'todos', + isInit: function () { + return true; + }.bind(_this), + type: 'jsonp', + options: function () { + return { + method: 'GET', + uri: 'https://a0ee9135-6a7f-4c0f-a215-f0f247ad907d.mock.pstmn.io', + }; + }.bind(_this), + dataHandler: function dataHandler(data) { + return data.data; + }, + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + {__$$evalArray(() => this.dataSourceMap.todos.data).map((item, index) => + ((__$$context) => ( + <div> + <Switch + checkedChildren="开" + unCheckedChildren="关" + checked={__$$eval(() => item.done)} + /> + </div> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </div> + ); + } +} + +export default $$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo9-datasource-engine/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/package.json new file mode 100644 index 0000000000..fb35f3cf46 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/package.json @@ -0,0 +1,46 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode-materials": "0.9.4", + "@alife/mc-assets-1935": "0.1.42", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..922ad47ad8 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,822 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Button, + Typography, + Form, + Select, + Input, + ConfigProvider, + Tooltip, + Empty, +} from '@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js'; + +import { + AliAutoDiv, + AliAutoSearchTable, +} from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const AliAutoDivDefault = AliAutoDiv.default; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + }; + + this.__jp__init(); + this.statusDesc = { + 0: '失败', + 1: '成功', + 2: '构建中', + 3: '构建超时', + }; + this.pageParams = {}; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + if (window.arsenal) { + this.$router = new window.jianpin.ArsenalRouter({ + app: this.props.microApp, + }); + } else { + this.$router = new window.jianpin.ArsenalRouter(); + } + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initConfig() { + /*...*/ + } + + __jp__initUtils() { + this.$utils = { + message: window.jianpin.utils.message, + axios: window.jianpin.utils.axios, + moment: window.jianpin.utils.moment, + }; + } + + fetchPkgs() { + /*...*/ + } + + onPageChange(pageIndex, pageSize) { + this.pageParams = { + pageIndex, + pageSize, + }; + this.fetchPkgs(); + } + + renderTime(time) { + return this.$utils.moment(time).format('YYYY-MM-DD HH:mm'); + } + + renderUserName(user) { + return user.user_name; + } + + reload() { + /*...*/ + } + + handleResult() { + /*...*/ + } + + handleDetail() { + // 跳转详情页面 TODO + } + + onResultCancel() { + this.setState({ + resultVisible: false, + }); + } + + formatResult(item) { + if (!item) { + return '暂无结果'; + } + const { channel, plat, version, status } = item; + return [channel, plat, version, status].join('-'); + } + + handleDownload() { + /*...*/ + } + + onFinish() { + /*...*/ + } + + componentDidMount() { + this.$ds.resolve('PROJECTS', { + params: { + size: 5000, + }, + }); + // if (this.state.init === false) { + // this.setState({ + // init: true, + // }); + // } + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="查看结果" + visible={__$$eval(() => this.state.resultVisible)} + footer={ + <Button + type="primary" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 确定 + </Button> + } + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + width="720px" + centered={true} + > + {__$$evalArray(() => this.state.results).map((item, index) => + ((__$$context) => ( + <AliAutoDivDefault style={{ width: '100%' }}> + {!!__$$eval( + () => + __$$context.state.results && + __$$context.state.results.length > 0 + ) && ( + <AliAutoDivDefault + style={{ + width: '100%', + textAlign: 'left', + marginBottom: '10px', + }} + > + <Button + type="primary" + size="small" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.handleDownload.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(__$$context)} + > + 下载全部 + </Button> + </AliAutoDivDefault> + )} + <Typography.Text> + {__$$eval(() => __$$context.formatResult(item))} + </Typography.Text> + {!!__$$eval(() => item.download_link) && ( + <Typography.Link + href={__$$eval(() => item.download_link)} + target="_blank" + > + {' '} + - 点击下载 + </Typography.Link> + )} + {!!__$$eval(() => item.release_notes) && ( + <Typography.Link + href={__$$eval(() => item.release_notes)} + target="_blank" + > + {' '} + - 跳转发布节点 + </Typography.Link> + )} + </AliAutoDivDefault> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onFinish.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + layout="inline" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label="项目名称/渠道号" name="channel_id"> + <Select + style={{ width: '280px' }} + options={__$$eval(() => this.state.projects)} + showArrow={true} + tokenSeparators={[]} + showSearch={true} + /> + </Form.Item> + <Form.Item label="版本号" name="buildId"> + <Input + placeholder="请输入" + style={{ width: '280px' }} + size="middle" + /> + </Form.Item> + <Form.Item label="构建人" name="user_id"> + <Select + style={{ width: 200 }} + options={[ + { label: 'A', value: 'A' }, + { label: 'B', value: 'B' }, + { label: 'C', value: 'C' }, + ]} + showSearch={true} + /> + </Form.Item> + <Form.Item label="ID" name="id"> + <Input placeholder="请输入" style={{ width: '160px' }} /> + </Form.Item> + <Form.Item wrapperCol={{ offset: 6 }}> + <Button type="primary" htmlType="submit"> + 查询 + </Button> + </Form.Item> + </Form> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <ConfigProvider locale="zh-CN"> + {!!__$$eval( + () => + !this.state.isSearch || + (this.state.isSearch && this.state.pkgs.length > 0) + ) && ( + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.pkgs)} + columns={[ + { + title: 'ID', + dataIndex: 'id', + key: 'name', + width: 80, + }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: (text, record, index) => + ((__$$context) => + __$$evalArray(() => text.split(',')).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ display: 'block' }} + > + {__$$eval(() => item)} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: (text, record, index) => + ((__$$context) => ( + <Tooltip + title={__$$evalArray(() => text || []).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ + display: 'block', + color: '#FFFFFF', + }} + > + {__$$eval( + () => + item.channelId + + ' / ' + + item.version + )} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + )} + > + <Typography.Text> + {__$$eval(() => text[0].version)} + </Typography.Text> + </Tooltip> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { title: '构建Job', dataIndex: 'job_name', width: 180 }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: (text, record, index) => + ((__$$context) => [ + <Typography.Text> + {__$$eval(() => __$$context.statusDesc[text])} + </Typography.Text>, + !!__$$eval(() => text === 2) && ( + <Icon + type="SyncOutlined" + size={16} + spin={true} + style={{ marginLeft: '10px' }} + /> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: function () { + return this.renderTime.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: function () { + return this.renderUserName.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href={__$$eval(() => text)} + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href="http://rivermap.alibaba.net/dashboard/testExecute" + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { title: '触发源', dataIndex: 'source', width: 120 }, + { + title: '详情', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleDetail.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleResult.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + ghost={false} + href={__$$eval(() => text)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: (text, record, index) => + ((__$$context) => ( + <Button + type="text" + children="" + icon={ + <Icon + type="ReloadOutlined" + size={14} + color="#0593d3" + style={{ + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }} + spin={false} + /> + } + shape="circle" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.reload.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + /> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + fixed: 'right', + }, + ]} + actions={[]} + pagination={{ + total: __$$eval(() => this.state.total), + defaultPageSize: 8, + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + }} + scrollX={1200} + /> + )} + </ConfigProvider> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => this.state.pkgs.length < 1 && this.state.isSearch + ) && <Empty description="暂无数据" />} + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_10-jsslot/schema.json5 diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/.browserslistrc b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/.browserslistrc new file mode 100644 index 0000000000..55a130413d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/.browserslistrc @@ -0,0 +1,3 @@ +defaults +ios_saf 9 + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/.gitignore diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/README.md new file mode 100644 index 0000000000..6d9dd75215 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/README.md @@ -0,0 +1 @@ +This project is generated by lowcode-code-generator & lowcode-solution-icejs3. \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/ice.config.mts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/ice.config.mts new file mode 100644 index 0000000000..e1d8a28141 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/ice.config.mts @@ -0,0 +1,90 @@ +import { join } from 'path'; +import { defineConfig } from '@ice/app'; +import _ from 'lodash'; +import fusion from '@ice/plugin-fusion'; +import locales from '@ice/plugin-moment-locales'; +import type { Plugin } from '@ice/app/esm/types'; + +interface PluginOptions { + id: string; +} + +const plugin: Plugin<PluginOptions> = (options) => ({ + // name 可选,插件名称 + name: 'plugin-name', + // setup 必选,用于定制工程构建配置 + setup: ({ onGetConfig, modifyUserConfig }) => { + modifyUserConfig('codeSplitting', 'page'); + + onGetConfig((config) => { + config.entry = { + web: join(process.cwd(), '.ice/entry.client.tsx'), + }; + + config.cssFilename = '[name].css'; + + config.configureWebpack = config.configureWebpack || []; + config.configureWebpack?.push((webpackConfig) => { + if (webpackConfig.output) { + webpackConfig.output.filename = '[name].js'; + webpackConfig.output.chunkFilename = '[name].js'; + } + return webpackConfig; + }); + + config.swcOptions = _.merge(config.swcOptions, { + compilationConfig: { + jsc: { + transform: { + react: { + runtime: 'classic', + }, + }, + }, + }, + }); + + // 解决 webpack publicPath 问题 + config.transforms = config.transforms || []; + config.transforms.push((source: string, id: string) => { + if (id.includes('.ice/entry.client.tsx')) { + let code = ` + if (!__webpack_public_path__?.startsWith('http') && document.currentScript) { + // @ts-ignore + __webpack_public_path__ = document.currentScript.src.replace(/^(.*\\/)[^/]+$/, '$1'); + window.__ICE_ASSETS_MANIFEST__ = window.__ICE_ASSETS_MANIFEST__ || {}; + window.__ICE_ASSETS_MANIFEST__.publicPath = __webpack_public_path__; + } + `; + code += source; + return { code }; + } + }); + }); + }, +}); + +// The project config, see https://v3.ice.work/docs/guide/basic/config +const minify = process.env.NODE_ENV === 'production' ? 'swc' : false; +export default defineConfig(() => ({ + ssr: false, + ssg: false, + minify, + + externals: { + react: 'React', + 'react-dom': 'ReactDOM', + 'react-dom/client': 'ReactDOM', + '@alifd/next': 'Next', + lodash: 'var window._', + '@alilc/lowcode-engine': 'var window.AliLowCodeEngine', + }, + plugins: [ + fusion({ + importStyle: 'sass', + }), + locales(), + plugin(), + ], +})); + diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/package.json new file mode 100644 index 0000000000..89f693efcb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/package.json @@ -0,0 +1,46 @@ +{ + "name": "icejs3-demo-app", + "version": "0.1.5", + "description": "icejs 3 轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router": "^6.9.0", + "react-router-dom": "^6.9.0", + "intl-messageformat": "^9.3.6", + "@alifd/next": "1.26.15", + "@ice/runtime": "~1.1.0", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode-materials": "0.11.0", + "@alife/mc-assets-1935": "0.1.43", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/app": "~3.1.0", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "@types/node": "^18.11.17", + "@ice/plugin-fusion": "^1.0.1", + "@ice/plugin-moment-locales": "^1.0.0", + "eslint": "^6.0.1", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "ice start", + "build": "ice build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "engines": { + "node": ">=14.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/app.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/app.ts new file mode 100644 index 0000000000..6d5856292d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/app.ts @@ -0,0 +1,13 @@ +import { defineAppConfig } from 'ice'; + +// App config, see https://v3.ice.work/docs/guide/basic/app +export default defineAppConfig(() => ({ + // Set your configs here. + app: { + rootId: 'App', + }, + router: { + type: 'browser', + basename: '/', + }, +})); diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/constants.js diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/document.tsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/document.tsx new file mode 100644 index 0000000000..aff0231d95 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/document.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Meta, Title, Links, Main, Scripts } from 'ice'; + +export default function Document() { + return ( + <html> + <head> + <meta charSet="utf-8" /> + <meta name="description" content="ice.js 3 lite scaffold" /> + <link rel="icon" href="/favicon.ico" /> + <link rel="stylesheet" href="//alifd.alicdn.com/npm/@alifd/next/1.21.16/next.min.css" /> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" /> + <Meta /> + <Title /> + <Links /> + </head> + <body> + <Main /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react/18.2.0/umd/react.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/react-dom/18.2.0/umd/react-dom.development.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/??react-router/6.9.0/react-router.production.min.js,react-router-dom/6.9.0/react-router-dom.production.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/alifd__next/1.26.22/next.min.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/code/lib/prop-types/15.7.2/prop-types.js" /> + <script crossOrigin="anonymous" src="//g.alicdn.com/platform/c/??lodash/4.6.1/lodash.min.js,immutable/3.7.6/dist/immutable.min.js" /> + <Scripts /> + </body> + </html> + ); +} \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss new file mode 100644 index 0000000000..dad05a263f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss @@ -0,0 +1,20 @@ + +.logo{ + display: flex; + align-items: center; + justify-content: center; + color: #FF7300; + font-weight: bold; + font-size: 14px; + line-height: 22px; + + &:visited, &:link { + color: #FF7300; + } + + img { + height: 24px; + margin-right: 10px; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx new file mode 100644 index 0000000000..911998b0d3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Link, useLocation } from 'ice'; +import { Nav } from '@alifd/next'; +import { asideMenuConfig } from '../../menuConfig'; + +const { SubNav } = Nav; +const NavItem = Nav.Item; + +function getNavMenuItems(menusData) { + if (!menusData) { + return []; + } + + return menusData + .filter(item => item.name && !item.hideInMenu) + .map((item, index) => getSubMenuOrItem(item, index)); +} + +function getSubMenuOrItem(item, index) { + if (item.children && item.children.some(child => child.name)) { + const childrenItems = getNavMenuItems(item.children); + + if (childrenItems && childrenItems.length > 0) { + const subNav = ( + <SubNav key={index} icon={item.icon} label={item.name}> + {childrenItems} + </SubNav> + ); + return subNav; + } + + return null; + } + + const navItem = ( + <NavItem key={item.path} icon={item.icon}> + <Link to={item.path}>{item.name}</Link> + </NavItem> + ); + return navItem; +} + +const Navigation = (props, context) => { + const location = useLocation(); + const { pathname } = location; + const { isCollapse } = context; + return ( + <Nav + type="primary" + selectedKeys={[pathname]} + defaultSelectedKeys={[pathname]} + embeddable + openMode="single" + iconOnly={isCollapse} + hasArrow={false} + mode={isCollapse ? 'popup' : 'inline'} + > + {getNavMenuItems(asideMenuConfig)} + </Nav> + ); +}; + +Navigation.contextTypes = { + isCollapse: PropTypes.bool, +}; +export default Navigation; + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..5630342f37 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,976 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Button, + Typography, + Form, + Select, + Input, + Tooltip, + Icon, + Empty, +} from '@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js'; + +import { + AliAutoDiv, + AliAutoSearchTable, +} from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const AliAutoDivDefault = AliAutoDiv.default; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + userOptions: [], + searchValues: { user_id: '', channel_id: '' }, + }; + + this.__jp__init(); + this.statusDesc = { + 0: '失败', + 1: '成功', + 2: '构建中', + 3: '构建超时', + }; + this.pageParams = {}; + this.searchParams = {}; + this.userTimeout = null; + this.currentUser = null; + this.notFoundContent = null; + this.projectTimeout = null; + this.currentProject = null; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + /*...*/ + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initConfig() { + /*...*/ + } + + __jp__initUtils() { + /*...*/ + } + + setSearchItem() { + /*...*/ + } + + fetchProject() { + /*...*/ + } + + handleProjectSearch() { + /*...*/ + } + + handleProjectChange(id) { + this.setSearchItem({ + channel_id: id, + }); + } + + fetchUser() { + /*...*/ + } + + handleUserSearch() { + /*...*/ + } + + handleUserChange(user) { + console.log('debug user', user); + this.setSearchItem({ + user_id: user, + }); + } + + fetchPkgs() { + /*...*/ + } + + onPageChange(pageIndex, pageSize) { + this.pageParams = { + pageIndex, + pageSize, + }; + this.fetchPkgs(); + } + + renderTime(time) { + return this.$utils.moment(time).format('YYYY-MM-DD HH:mm'); + } + + renderUserName(user) { + return user.user_name; + } + + reload() { + /*...*/ + } + + handleResult() { + /*...*/ + } + + handleDetail() { + /*...*/ + } + + onResultCancel() { + /*...*/ + } + + formatResult() { + /*...*/ + } + + handleDownload() { + /*...*/ + } + + onFinish() { + /*...*/ + } + + componentDidMount() { + this.$ds.resolve('PROJECTS'); + if (this.userTimeout) { + clearTimeout(this.userTimeout); + this.userTimeout = null; + } + if (this.projectTimeout) { + clearTimeout(this.projectTimeout); + this.projectTimeout = null; + } + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="查看结果" + visible={__$$eval(() => this.state.resultVisible)} + footer={ + <Button + type="primary" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 确定 + </Button> + } + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + width="720px" + centered={true} + closable={true} + keyboard={true} + mask={true} + maskClosable={true} + > + <AliAutoDivDefault style={{ width: '100%' }}> + {!!__$$eval( + () => this.state.results && this.state.results.length > 0 + ) && ( + <AliAutoDivDefault + style={{ + width: '100%', + textAlign: 'left', + marginBottom: '16px', + }} + > + <Button + type="primary" + size="small" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.handleDownload.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 下载全部 + </Button> + </AliAutoDivDefault> + )} + {__$$evalArray(() => this.state.results).map((item, index) => + ((__$$context) => ( + <AliAutoDivDefault style={{ width: '100%', marginTop: '10px' }}> + <Typography.Text> + {__$$eval(() => __$$context.formatResult(item))} + </Typography.Text> + {!!__$$eval(() => item.download_link) && ( + <Typography.Link + href={__$$eval(() => item.download_link)} + target="_blank" + > + {' '} + - 点击下载 + </Typography.Link> + )} + {!!__$$eval(() => item.release_notes) && ( + <Typography.Link + href={__$$eval(() => item.release_notes)} + target="_blank" + > + {' '} + - 跳转发布节点 + </Typography.Link> + )} + </AliAutoDivDefault> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </AliAutoDivDefault> + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface', style: { padding: '' } }} + footer={null} + minHeight="100vh" + contentProps={{ noPadding: false, background: 'transparent' }} + > + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <AliAutoDivDefault style={{ width: '100%', display: 'flex' }}> + <AliAutoDivDefault style={{ flex: '1' }}> + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onFinish.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + layout="inline" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + colon={true} + labelAlign="right" + preserve={true} + scrollToFirstError={true} + size="middle" + values={__$$eval(() => this.state.searchValues)} + > + <Form.Item + label="项目名称/渠道号" + name="channel_id" + labelAlign="right" + colon={true} + > + <Select + style={{ width: '320px' }} + options={__$$eval(() => this.state.projects)} + showArrow={false} + tokenSeparators={[]} + showSearch={true} + defaultActiveFirstOption={true} + size="middle" + bordered={true} + filterOption={true} + optionFilterProp="label" + allowClear={true} + placeholder="请输入项目名称/渠道号" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleProjectChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleProjectSearch', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: true }, + { name: 'onSelect', disabled: false }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }} + onChange={function () { + this.handleProjectChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onSearch={function () { + this.handleProjectSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Form.Item> + <Form.Item label="版本号" name="buildId"> + <Input + placeholder="请输入版本号" + style={{ width: '180px' }} + size="middle" + bordered={true} + /> + </Form.Item> + <Form.Item label="构建人" name="user_id"> + <Select + style={{ width: '210px' }} + options={__$$eval(() => this.state.userOptions)} + showSearch={true} + defaultActiveFirstOption={false} + size="middle" + bordered={true} + filterOption={true} + optionFilterProp="label" + notFoundContent={__$$eval( + () => this.userNotFoundContent + )} + showArrow={false} + placeholder="请输入构建人" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleUserChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleUserSearch', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: true }, + { name: 'onSelect', disabled: false }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }} + onChange={function () { + this.handleUserChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onSearch={function () { + this.handleUserSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + allowClear={true} + /> + </Form.Item> + <Form.Item + label="ID" + name="id" + labelAlign="right" + colon={true} + > + <Input + placeholder="请输入ID" + style={{ width: '180px' }} + bordered={true} + size="middle" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: 6 }} + labelAlign="right" + colon={true} + style={{ flex: '1', textAlign: 'right' }} + > + <Button + type="primary" + htmlType="submit" + shape="default" + size="middle" + > + 查询 + </Button> + </Form.Item> + </Form> + </AliAutoDivDefault> + <AliAutoDivDefault style={{}}> + <Button + type="link" + htmlType="button" + shape="default" + size="middle" + > + 新增打包 + </Button> + </AliAutoDivDefault> + </AliAutoDivDefault> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock + childTotalColumns={12} + mode="inset" + layoutmode="O" + autolayout="(12|1)" + > + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => + !this.state.isSearch || + (this.state.isSearch && this.state.pkgs.length > 0) + ) && ( + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.pkgs)} + columns={[ + { title: 'ID', dataIndex: 'id', key: 'name', width: 80 }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: (text, record, index) => + ((__$$context) => + __$$evalArray(() => text.split(',')).map( + (item, index) => + ((__$$context) => ( + <Typography.Text style={{ display: 'block' }}> + {__$$eval(() => item)} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: (text, record, index) => + ((__$$context) => ( + <Tooltip + title={__$$evalArray(() => text || []).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ + display: 'block', + color: '#FFFFFF', + }} + > + {__$$eval( + () => + item.channelId + ' / ' + item.version + )} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + )} + > + <Typography.Text> + {__$$eval(() => text[0].version)} + </Typography.Text> + </Tooltip> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { title: '构建Job', dataIndex: 'job_name', width: 180 }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: (text, record, index) => + ((__$$context) => [ + <Typography.Text> + {__$$eval(() => __$$context.statusDesc[text])} + </Typography.Text>, + !!__$$eval(() => text === 2) && ( + <Icon + type="SyncOutlined" + size={16} + spin={true} + style={{ marginLeft: '10px' }} + /> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: function () { + return this.renderTime.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: function () { + return this.renderUserName.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href={__$$eval(() => text)} + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href="http://rivermap.alibaba.net/dashboard/testExecute" + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { title: '触发源', dataIndex: 'source', width: 120 }, + { + title: '详情', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleDetail.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleResult.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + ghost={false} + href={__$$eval(() => text)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: (text, record, index) => + ((__$$context) => ( + <Button + type="text" + children="" + icon={ + <Icon + type="ReloadOutlined" + size={14} + color="#0593d3" + style={{ + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }} + spin={false} + /> + } + shape="circle" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.reload.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + /> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + fixed: 'right', + }, + ]} + actions={[]} + pagination={{ + total: __$$eval(() => this.state.total), + defaultPageSize: 10, + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + defaultPageIndex: 1, + }} + scrollX={1200} + isPagination={true} + /> + )} + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock + childTotalColumns={12} + mode="inset" + layoutmode="O" + autolayout="(12|1)" + > + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => this.state.pkgs.length < 1 && this.state.isSearch + ) && <Empty description="暂无数据" />} + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/layout.jsx b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/layout.jsx new file mode 100644 index 0000000000..50fbb2d1f1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/pages/layout.jsx @@ -0,0 +1,10 @@ +import { Outlet } from 'ice'; +import BasicLayout from '@/layouts/BasicLayout'; + +export default function Layout() { + return ( + <BasicLayout> + <Outlet /> + </BasicLayout> + ); +} diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/typings.d.ts b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/typings.d.ts new file mode 100644 index 0000000000..a9f8de7ceb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/typings.d.ts @@ -0,0 +1,9 @@ +/// <reference types="@ice/app/types" /> + +export {}; +declare global { + interface Window { + g_config: Record<string, any>; + } +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/icejs3-app/demo_11-jsslot-2/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/package.json new file mode 100644 index 0000000000..fd03ed9bc5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/package.json @@ -0,0 +1,29 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo01/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/schema.json5 new file mode 100644 index 0000000000..94c3a2fb7b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo01/schema.json5 @@ -0,0 +1,54 @@ +{ + // 本例是一个非常简单的 Hello world 页面 + version: '1.0.0', + componentsMap: [ + { + componentName: 'Page', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'Page', + }, + { + componentName: 'Text', + package: 'rax-text', + version: '^1.0.0', + destructuring: false, + exportName: 'Text', + }, + ], + componentsTree: [ + { + componentName: 'Page', + props: {}, + lifeCycles: {}, + fileName: 'home', + meta: { + router: '/', + }, + dataSource: { + list: [], + }, + children: [ + { + componentName: 'Text', + props: {}, + children: 'Hello world!', + }, + ], + }, + ], + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + }, + meta: { + name: 'Rax App Demo', + git_group: 'demo-group', + project_name: 'demo-project', + description: '这是一个示例应用', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/package.json new file mode 100644 index 0000000000..4d9a779880 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/package.json @@ -0,0 +1,35 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0", + "rax-image": "^1.0.0", + "moment": "*", + "lodash": "*", + "universal-toast": "^1.2.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx new file mode 100644 index 0000000000..06e39454d0 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/pages/Home/index.jsx @@ -0,0 +1,347 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:rax 框架的导出名和各种组件名除外。 +import { createElement, Component } from 'rax'; +import { getSearchParams as __$$getSearchParams } from 'rax-app'; + +import Page from 'rax-view'; + +import View from 'rax-view'; + +import Text from 'rax-text'; + +import Image from 'rax-image'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import { isMiniApp as __$$isMiniApp } from 'universal-env'; + +import __$$constants from '../../constants'; + +import * as __$$i18n from '../../i18n'; + +import __$$projectUtils from '../../utils'; + +import './index.css'; + +class Home$$Page extends Component { + state = { + clickCount: 0, + user: { + name: '张三', + age: 18, + avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png', + }, + orders: [ + { + title: '【小米智能生活】米家扫地机器人家用全自动扫拖一体机拖地吸尘器', + price: 1799, + coverUrl: 'https://gw.alicdn.com/tfs/TB1dGVlRfb2gK0jSZK9XXaEgFXa-258-130.png', + }, + { + title: '【限时下单立减】Apple/苹果 iPhone 11 4G全网通智能手机正品苏宁易购官方旗舰店苹果11', + price: 4999, + coverUrl: 'https://gw.alicdn.com/tfs/TB18gdJddTfau8jSZFwXXX1mVXa-1298-1202.png', + }, + ], + }; + + _methods = this._defineMethods(); + + _context = this._createContext(); + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this._context, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(__$$getSearchParams()), + fetch: __$$createFetchRequestHandler(), + }, + }); + + _utils = this._defineUtils(); + + _lifeCycles = this._defineLifeCycles(); + + constructor(props, context) { + super(props); + + __$$i18n._inject2(this); + } /* end of constructor */ + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + this._lifeCycles.didMount(); + } /* end of componentDidMount */ + + componentWillUnmount() { + this._lifeCycles.willUnmount(); + } /* end of componentWillUnmount */ + + render() { + const __$$context = this._context; + const { + state, + setState, + dataSourceMap, + reloadDataSource, + utils, + constants, + i18n, + i18nFormat, + getLocale, + setLocale, + } = __$$context; + + return ( + <Page> + <View> + <Text>Demo data source logic</Text> + </View> + <View> + <Text>=== User Info: ===</Text> + </View> + {!!__$$eval(() => __$$context.state.user) && ( + <View style={{ flexDirection: 'row' }}> + <Image + source={{ uri: __$$eval(() => __$$context.state.user.avatar) }} + style={{ width: '32px', height: '32px' }} + /> + <View onClick={__$$context.hello}> + <Text>{__$$eval(() => __$$context.state.user.name)}</Text> + <Text>{__$$eval(() => __$$context.state.user.age)}岁</Text> + </View> + </View> + )} + <View> + <Text>=== Orders: ===</Text> + </View> + {__$$evalArray(() => __$$eval(() => __$$context.state.orders)).map((order, index) => + ((__$$context) => ( + <View + style={{ flexDirection: 'row' }} + data-order={order} + onClick={(...__$$args) => { + if (__$$isMiniApp) { + const __$$event = __$$args[0]; + const order = __$$event.target.dataset.order; + return function () { + __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); + }.apply(this, __$$args); + } else { + return function () { + __$$context.utils.recordEvent(`CLICK_ORDER`, order.title); + }.apply(this, __$$args); + } + }} + > + <View> + <Image source={{ uri: __$$eval(() => order.coverUrl) }} style={{ width: '80px', height: '60px' }} /> + </View> + <View> + <Text>{__$$eval(() => order.title)}</Text> + <Text>{__$$eval(() => __$$context.utils.formatPrice(order.price, '元'))}</Text> + </View> + </View> + ))(__$$createChildContext(__$$context, { order, index })), + )} + <View + onClick={function () { + __$$context.setState({ + clickCount: __$$context.state.clickCount + 1, + }); + }} + > + <Text>点击次数:{__$$eval(() => __$$context.state.clickCount)}(点击加 1)</Text> + </View> + <View> + <Text>操作提示:</Text> + <Text>1. 点击会员名,可以弹出 Toast "Hello xxx!"</Text> + <Text>2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示</Text> + <Text>3. 最下面的【点击次数】,点一次应该加 1</Text> + </View> + </Page> + ); + } /* end of render */ + + _createContext() { + const self = this; + const context = { + get state() { + return self.state; + }, + setState(newState, callback) { + self.setState(newState, callback); + }, + get dataSourceMap() { + return self._dataSourceEngine.dataSourceMap || {}; + }, + async reloadDataSource() { + await self._dataSourceEngine.reloadDataSource(); + }, + get utils() { + return self._utils; + }, + get page() { + return context; + }, + get component() { + return context; + }, + get props() { + return self.props; + }, + get constants() { + return __$$constants; + }, + i18n: __$$i18n.i18n, + i18nFormat: __$$i18n.i18nFormat, + getLocale: __$$i18n.getLocale, + setLocale(locale) { + __$$i18n.setLocale(locale); + self.forceUpdate(); + }, + ...this._methods, + }; + + return context; + } + + _defineDataSourceConfig() { + const __$$context = this._context; + return { + list: [ + { + errorHandler: function (err) { + setTimeout(() => { + __$$context.setState({ + __refresh: Date.now() + Math.random(), + }); + }, 0); + throw err; + }, + id: 'urlParams', + type: 'urlParams', + isInit: true, + options: function () { + return undefined; + }, + }, + { + errorHandler: function (err) { + setTimeout(() => { + __$$context.setState({ + __refresh: Date.now() + Math.random(), + }); + }, 0); + throw err; + }, + id: 'user', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }; + }, + dataHandler: function (response) { + if (!response.success) { + throw new Error(response.message); + } + return response.data; + }, + isInit: true, + }, + { + errorHandler: function (err) { + setTimeout(() => { + __$$context.setState({ + __refresh: Date.now() + Math.random(), + }); + }, 0); + throw err; + }, + id: 'orders', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: __$$context.state.user.ordersApiUri, + isSync: true, + }; + }, + dataHandler: function (response) { + if (!response.success) { + throw new Error(response.message); + } + return response.data.result; + }, + isInit: true, + }, + ], + dataHandler: function (dataMap) { + console.info('All datasources loaded:', dataMap); + }, + }; + } + + _defineUtils() { + return { + ...__$$projectUtils, + }; + } + + _defineLifeCycles() { + const __$$lifeCycles = { + didMount: function didMount() { + this.utils.Toast.show(`Hello ${this.state.user.name}!`); + }, + + willUnmount: function didMount() { + this.utils.Toast.show(`Bye, ${this.state.user.name}!`); + }, + }; + + // 为所有的方法绑定上下文 + Object.entries(__$$lifeCycles).forEach(([lifeCycleName, lifeCycleMethod]) => { + if (typeof lifeCycleMethod === 'function') { + __$$lifeCycles[lifeCycleName] = (...args) => { + return lifeCycleMethod.apply(this._context, args); + }; + } + }); + + return __$$lifeCycles; + } + + _defineMethods() { + return { + hello: function hello() { + this.utils.Toast.show(`Hello ${this.state.user.name}!`); + console.log(`Hello ${this.state.user.name}!`); + }, + }; + } +} + +export default Home$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + return Object.assign({}, oldContext, ext); +} diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo02/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/schema.json5 new file mode 100644 index 0000000000..59f1a8bbc2 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo02/schema.json5 @@ -0,0 +1,374 @@ +{ + // 本例是一个比较复杂的,带有循环和条件渲染的,以及有各种事件处理函数的页面 + version: '1.0.0', + componentsMap: [ + { + componentName: 'View', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'View', + }, + { + componentName: 'Text', + package: 'rax-text', + version: '^1.0.0', + destructuring: false, + exportName: 'Text', + }, + { + componentName: 'Image', + package: 'rax-image', + version: '^1.0.0', + destructuring: false, + exportName: 'Image', + }, + { + componentName: 'Page', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'Page', + }, + ], + componentsTree: [ + { + componentName: 'Page', + fileName: 'home', + meta: { + router: '/', + }, + state: { + clickCount: 0, + user: { name: '张三', age: 18, avatar: 'https://gw.alicdn.com/tfs/TB1Ui9BMkY2gK0jSZFgXXc5OFXa-50-50.png' }, + orders: [ + { + title: '【小米智能生活】米家扫地机器人家用全自动扫拖一体机拖地吸尘器', + price: 1799, + coverUrl: 'https://gw.alicdn.com/tfs/TB1dGVlRfb2gK0jSZK9XXaEgFXa-258-130.png', + }, + { + title: '【限时下单立减】Apple/苹果 iPhone 11 4G全网通智能手机正品苏宁易购官方旗舰店苹果11', + price: 4999, + coverUrl: 'https://gw.alicdn.com/tfs/TB18gdJddTfau8jSZFwXXX1mVXa-1298-1202.png', + }, + ], + }, + props: {}, + lifeCycles: { + didMount: { + type: 'JSExpression', + value: 'function didMount(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n}', + }, + willUnmount: { + type: 'JSExpression', + value: 'function didMount(){\n this.utils.Toast.show(`Bye, ${this.state.user.name}!`);\n}', + }, + }, + methods: { + hello: { + type: 'JSExpression', + value: 'function hello(){\n this.utils.Toast.show(`Hello ${this.state.user.name}!`);\n console.log(`Hello ${this.state.user.name}!`); }', + }, + }, + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/user + { + id: 'user', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data;\n}', + }, + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders + { + id: 'orders', + type: 'fetch', + options: { + method: 'GET', + uri: { + type: 'JSExpression', + value: 'this.state.user.ordersApiUri', + }, + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.success){\n throw new Error(response.message);\n }\n return response.data.result;\n}', + }, + }, + ], + dataHandler: { + type: 'JSExpression', + value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', + }, + }, + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Text', + props: {}, + children: 'Demo data source logic', + }, + ], + }, + { + componentName: 'View', + children: [ + { + componentName: 'Text', + props: {}, + children: '=== User Info: ===', + }, + ], + }, + { + componentName: 'View', + condition: { + type: 'JSExpression', + value: 'this.state.user', + }, + props: { + style: { flexDirection: 'row' }, + }, + children: [ + { + componentName: 'Image', + props: { + source: { + uri: { + type: 'JSExpression', + value: 'this.state.user.avatar', + }, + }, + style: { + width: '32px', + height: '32px', + }, + }, + }, + { + componentName: 'View', + props: { + onClick: { + type: 'JSExpression', + value: 'this.hello', + }, + }, + children: [ + { + componentName: 'Text', + children: { + type: 'JSExpression', + value: 'this.state.user.name', + }, + }, + { + componentName: 'Text', + children: [ + { + type: 'JSExpression', + value: 'this.state.user.age', + }, + '岁', + ], + }, + ], + }, + ], + }, + { + componentName: 'View', + children: [ + { + componentName: 'Text', + props: {}, + children: '=== Orders: ===', + }, + ], + }, + { + componentName: 'View', + loop: { + type: 'JSExpression', + value: 'this.state.orders', + }, + loopArgs: ['order', 'index'], + props: { + style: { flexDirection: 'row' }, + onClick: { + type: 'JSExpression', + value: 'function(){ this.utils.recordEvent(`CLICK_ORDER`, this.order.title) }', + }, + }, + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Image', + props: { + source: { + uri: { + type: 'JSExpression', + value: 'this.order.coverUrl', + }, + }, + style: { + width: '80px', + height: '60px', + }, + }, + }, + ], + }, + { + componentName: 'View', + children: [ + { + componentName: 'Text', + children: { + type: 'JSExpression', + value: 'this.order.title', + }, + }, + { + componentName: 'Text', + children: { + type: 'JSExpression', + value: 'this.utils.formatPrice(this.order.price, "元")', + }, + }, + ], + }, + ], + }, + { + componentName: 'View', + props: { + onClick: { + type: 'JSExpression', + value: 'function (){ this.setState({ clickCount: this.state.clickCount + 1 }) }', + }, + }, + children: [ + { + componentName: 'Text', + children: [ + '点击次数:', + { + type: 'JSExpression', + value: 'this.state.clickCount', + }, + '(点击加 1)', + ], + }, + ], + }, + { + componentName: 'View', + children: [ + { + componentName: 'Text', + props: {}, + children: '操作提示:', + }, + { + componentName: 'Text', + props: {}, + children: '1. 点击会员名,可以弹出 Toast "Hello xxx!"', + }, + { + componentName: 'Text', + props: {}, + children: '2. 点击订单,会记录点击的订单信息,并弹出 Toast 提示', + }, + { + componentName: 'Text', + props: {}, + children: '3. 最下面的【点击次数】,点一次应该加 1', + }, + ], + }, + ], + }, + ], + utils: [ + // 可以直接定义一个函数 + { + name: 'formatPrice', + type: 'function', + content: { + type: 'JSExpression', + value: 'function formatPrice(price, unit) { return Number(price).toFixed(2) + unit; }', + }, + }, + // 在 utils 里面也可以用 this 访问当前上下文: + { + name: 'recordEvent', + type: 'function', + content: { + type: 'JSExpression', + value: 'function recordEvent(eventName, eventDetail) { \n this.utils.Toast.show(`[EVENT]: ${eventName} ${eventDetail}`);\n console.log(`[EVENT]: ${eventName} (detail: %o) (user: %o)`, eventDetail, this.state.user); }', + }, + }, + // 也可以直接从 npm 包引入 (下例等价于 `import moment from 'moment';`) + { + name: 'moment', + type: 'npm', + content: { + package: 'moment', + version: '*', + exportName: 'moment', + }, + }, + // 可以引入子目录(下例等价于 `import clone from 'lodash/clone';`) + { + name: 'clone', + type: 'npm', + content: { + package: 'lodash', + version: '*', + exportName: 'clone', + destructuring: false, + main: '/clone', + }, + }, + { + name: 'Toast', + type: 'npm', + content: { + package: 'universal-toast', + version: '^1.2.0', + exportName: 'Toast', // TODO: 这个 exportName 是否可以省略?省略后默认是上一层的 name? + }, + }, + ], + css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}', + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + }, + meta: { + name: 'Rax App Demo', + git_group: 'demo-group', + project_name: 'demo-project', + description: '这是一个示例应用', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/package.json new file mode 100644 index 0000000000..58b97921b5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0", + "rax-link": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Detail/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/pages/List/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo03/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/schema.json5 new file mode 100644 index 0000000000..bed7c8092f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo03/schema.json5 @@ -0,0 +1,175 @@ +{ + // 本例是一个路由测试页面,里面有几个页面,相互之间有跳转关系的 + version: '1.0.0', + componentsMap: [ + { + componentName: 'View', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'View', + }, + { + componentName: 'Text', + package: 'rax-text', + version: '^1.0.0', + destructuring: false, + exportName: 'Text', + }, + { + componentName: 'Link', + package: 'rax-link', + version: '^1.0.0', + destructuring: false, + exportName: 'Link', + }, + { + componentName: 'Image', + package: 'rax-image', + version: '^1.0.0', + destructuring: false, + exportName: 'Image', + }, + { + componentName: 'Page', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'Page', + }, + ], + componentsTree: [ + { + componentName: 'Page', + fileName: 'home', + state: {}, + dataSource: { + list: [], + }, + meta: { + router: '/', + }, + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Text', + children: 'This is the Home Page', + }, + ], + }, + { + componentName: 'Link', + props: { + href: '#/list', + miniappHref: 'navigate:/pages/List/index', + }, + children: [ + { + componentName: 'Text', + children: 'Go To The List Page', + }, + ], + }, + ], + }, + { + componentName: 'Page', + fileName: 'list', + state: {}, + dataSource: { + list: [], + }, + meta: { + router: '/list', + }, + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Text', + children: 'This is the List Page', + }, + ], + }, + { + componentName: 'Link', + props: { + href: '#/detail', + miniappHref: 'navigate:/pages/Detail/index', + }, + children: [ + { + componentName: 'Text', + children: 'Go To The Detail Page', + }, + ], + }, + { + componentName: 'Link', + props: { + href: 'javascript:history.back();', + miniappHref: 'navigateBack:', + }, + children: [ + { + componentName: 'Text', + children: 'Go back', + }, + ], + }, + ], + }, + { + componentName: 'Page', + fileName: 'detail', + state: {}, + dataSource: { + list: [], + }, + meta: { + router: '/detail', + }, + children: [ + { + componentName: 'View', + children: [ + { + componentName: 'Text', + children: 'This is the Detail Page', + }, + ], + }, + { + componentName: 'Link', + props: { + href: 'javascript:history.back();', + miniappHref: 'navigateBack:', + }, + children: [ + { + componentName: 'Text', + children: 'Go back', + }, + ], + }, + ], + }, + ], + css: 'page,body{\n width: 750rpx;\n overflow-x: hidden;\n}', + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + }, + meta: { + name: 'Rax App Demo', + git_group: 'demo-group', + project_name: 'demo-project', + description: '这是一个示例应用', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/rax-app/demo04/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/package.json new file mode 100644 index 0000000000..56dda7653a --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "@alife/right-design-card": "*", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo04/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo04/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo04/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/package.json new file mode 100644 index 0000000000..fd03ed9bc5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/package.json @@ -0,0 +1,29 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo05/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/schema.json5 new file mode 100644 index 0000000000..483e527319 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo05/schema.json5 @@ -0,0 +1,72 @@ +{ + // 这是一个关于国际化的 schema 示例 + version: '1.0.0', + componentsMap: [ + { + componentName: 'Page', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'Page', + }, + { + componentName: 'Text', + package: 'rax-text', + version: '^1.0.0', + destructuring: false, + exportName: 'Text', + }, + ], + componentsTree: [ + { + componentName: 'Page', + props: {}, + lifeCycles: {}, + fileName: 'home', + meta: { + router: '/', + }, + dataSource: { + list: [], + }, + children: [ + { + componentName: 'Text', + props: { + onClick: { + type: 'JSFunction', + value: "function () {\n this.setLocale(this.getLocale() === 'en-US' ? 'zh-CN' : 'en-US');\n}", + }, + }, + children: [ + { + type: 'JSExpression', + value: 'this.i18n["hello-world"]', + }, + ], + }, + ], + }, + ], + i18n: { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, + }, + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + }, + meta: { + name: 'Rax App Demo', + git_group: 'demo-group', + project_name: 'demo-project', + description: '这是一个示例应用', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json new file mode 100644 index 0000000000..dc00ba429f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-table": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo06-jsslot/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo06-jsslot/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo06-jsslot/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json new file mode 100644 index 0000000000..fd03ed9bc5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/package.json @@ -0,0 +1,29 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo07-newline-in-props/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo07-newline-in-props/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo07-newline-in-props/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json new file mode 100644 index 0000000000..dc00ba429f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-table": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo08-jsslot-with-multiple-children/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo08-jsslot-with-multiple-children/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json new file mode 100644 index 0000000000..dc00ba429f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-table": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo09-jsslot-with-conditional-children/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo09-jsslot-with-conditional-children/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json new file mode 100644 index 0000000000..dc00ba429f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/package.json @@ -0,0 +1,30 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-table": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo10-jsslot-with-loop-children/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo10-jsslot-with-loop-children/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json new file mode 100644 index 0000000000..067cc161d9 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/package.json @@ -0,0 +1,33 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "@alilc/b6-page": "^0.1.0", + "@alilc/b6-text": "^0.1.0", + "@alilc/b6-compact-legao-builtin": "1.x", + "antd": "3.x", + "@alilc/b6-util-mocks": "1.x" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo11-utils-name-alias/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo11-utils-name-alias/schema.json5 diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/package.json new file mode 100644 index 0000000000..fd03ed9bc5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/package.json @@ -0,0 +1,29 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "rax-view": "^1.0.0", + "rax-text": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1ebb554860 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/i18n.js @@ -0,0 +1,75 @@ +const i18nConfig = { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, +}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/pages/Home/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo12-refs/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/schema.json5 new file mode 100644 index 0000000000..9a89838a12 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo12-refs/schema.json5 @@ -0,0 +1,73 @@ +{ + // 这是一个关于国际化的 schema 示例 + version: '1.0.0', + componentsMap: [ + { + componentName: 'Page', + package: 'rax-view', + version: '^1.0.0', + destructuring: false, + exportName: 'Page', + }, + { + componentName: 'Text', + package: 'rax-text', + version: '^1.0.0', + destructuring: false, + exportName: 'Text', + }, + ], + componentsTree: [ + { + componentName: 'Page', + props: {}, + lifeCycles: {}, + fileName: 'home', + meta: { + router: '/', + }, + dataSource: { + list: [], + }, + children: [ + { + componentName: 'Text', + props: { + ref: 'helloText', + onClick: { + type: 'JSFunction', + value: "function () {\n this.setLocale(this.getLocale() === 'en-US' ? 'zh-CN' : 'en-US');\n}", + }, + }, + children: [ + { + type: 'JSExpression', + value: 'this.i18n["hello-world"]', + }, + ], + }, + ], + }, + ], + i18n: { + 'zh-CN': { + 'hello-world': '你好,世界!', + }, + 'en-US': { + 'hello-world': 'Hello world!', + }, + }, + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + }, + meta: { + name: 'Rax App Demo', + git_group: 'demo-group', + project_name: 'demo-project', + description: '这是一个示例应用', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json new file mode 100644 index 0000000000..afadad8785 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/package.json @@ -0,0 +1,29 @@ +{ + "name": "rax-demo-app", + "private": true, + "version": "1.0.0", + "scripts": { + "start": "rax-app start", + "build": "rax-app build", + "eslint": "eslint --ext .js,.jsx ./", + "stylelint": "stylelint \"**/*.{css,scss,less}\"", + "prettier": "prettier **/* --write", + "lint": "npm run eslint && npm run stylelint" + }, + "dependencies": { + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-http-handler": "^1.0.0", + "universal-env": "^3.2.0", + "intl-messageformat": "^9.3.6", + "rax": "^1.1.0", + "rax-document": "^0.1.6", + "@alilc/lowcode-components": "^1.0.0" + }, + "devDependencies": { + "@iceworks/spec": "^1.0.0", + "rax-app": "^3.0.0", + "eslint": "^6.8.0", + "prettier": "^2.1.2", + "stylelint": "^13.7.2" + } +} diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/app.json diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/document/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/document/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/document/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/document/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/global.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/global.css similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/global.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/global.css diff --git a/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..a5dde6f77d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/i18n.js @@ -0,0 +1,68 @@ +const i18nConfig = {}; + +let locale = typeof navigator === 'object' && typeof navigator.language === 'string' ? navigator.language : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = i18nConfig[locale]?.[id] ?? i18nConfig[locale.replace('-', '_')]?.[id] ?? defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/components/Index/index.css b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.css similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/components/Index/index.css rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.css diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.jsx b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.jsx similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/pages/Example/index.jsx diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/rax-app/demo13-datasource-prop/schema.json5 diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.gitignore similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.gitignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.gitignore diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/package.json new file mode 100644 index 0000000000..d18604922d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/package.json @@ -0,0 +1,50 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.css rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..794ad46a48 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,205 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + fetch: __$$createFetchRequestHandler(), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + isInit: function () { + return undefined; + }.bind(_this), + options: function () { + return undefined; + }.bind(_this), + }, + { + id: 'user', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + { + id: 'orders', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data.result; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + dataHandler: function (dataMap) { + console.info('All datasources loaded:', dataMap); + }, + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item label="姓名:" name="name" initValue="李雷"> + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + {__$$evalArray(() => ['a', 'b', 'c']).map((item, index) => + ((__$$context) => + !!__$$eval(() => index >= 1) && ( + <Button type="primary" style={{ margin: '0 5px 0 5px' }}> + {__$$eval(() => item)} + </Button> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..47a0f2d417 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo1/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/schema.json5 new file mode 100644 index 0000000000..76c52fb5e8 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo1/schema.json5 @@ -0,0 +1,276 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button" + }, + { + "componentName": "Button.Group", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "Input", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Input" + }, + { + "componentName": "Form", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form" + }, + { + "componentName": "Form.Item", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form", + "subName": "Item" + }, + { + "componentName": "NumberPicker", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "NumberPicker" + }, + { + "componentName": "Select", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Select" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node$1", + "meta": { + "title": "测试", + "router": "/" + }, + "props": { + "ref": "outterView", + "autoLoading": true + }, + "fileName": "test", + "state": { + "text": "outter" + }, + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() { console.log('componentDidMount'); }" + } + }, + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/user + { + id: 'user', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', + }, + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders + { + id: 'orders', + type: 'fetch', + options: { + method: 'GET', + uri: "https://shs.xxx.com/mock/1458/demo/orders", + isSync: true, + }, + dataHandler: { + type: 'JSExpression', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', + }, + }, + ], + dataHandler: { + type: 'JSExpression', + value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', + }, + }, + "children": [ + { + "componentName": "Form", + "id": "node$2", + "props": { + "labelCol": { + "type": "JSExpression", + "value": "this.state.colNum" + }, + "style": {}, + "ref": "testForm" + }, + "children": [ + { + "componentName": "Form.Item", + "id": "node$3", + "props": { + "label": "姓名:", + "name": "name", + "initValue": "李雷" + }, + "children": [ + { + "componentName": "Input", + "id": "node$4", + "props": { + "placeholder": "请输入", + "size": "medium", + "style": { + "width": 320 + } + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$5", + "props": { + "label": "年龄:", + "name": "age", + "initValue": "22" + }, + "children": [ + { + "componentName": "NumberPicker", + "id": "node$6", + "props": { + "size": "medium", + "type": "normal" + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$7", + "props": { + "label": "职业:", + "name": "profession" + }, + "children": [ + { + "componentName": "Select", + "id": "node$8", + "props": { + "dataSource": [ + { + "label": "教师", + "value": "t" + }, + { + "label": "医生", + "value": "d" + }, + { + "label": "歌手", + "value": "s" + } + ] + } + } + ] + }, + { + "componentName": "Div", + "id": "node$9", + "props": { + "style": { + "textAlign": "center" + } + }, + "children": [ + { + "componentName": "Button.Group", + "id": "node$a", + "props": {}, + "children": [ + { + "componentName": "Button", + "id": "node$b", + "condition": { + "type": "JSExpression", + "value": "this.index >= 1" + }, + "loop": ["a", "b", "c"], + "props": { + "type": "primary", + "style": { + "margin": "0 5px 0 5px" + }, + }, + "children": [ + { + "type": "JSExpression", + "value": "this.item" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.xxx.com" + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + } + }, + "theme": { + "package": "@alife/theme-fusion", + "version": "^0.1.0", + "primary": "#ff9966" + } + }, + "meta": { + "name": "demo应用", + "git_group": "appGroup", + "project_name": "app_demo", + "description": "这是一个测试应用", + "spma": "spa23d", + "creator": "月飞" + } +} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json new file mode 100644 index 0000000000..d83d45e324 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/package.json @@ -0,0 +1,53 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/b6-page": "^0.1.0", + "@alilc/b6-text": "^0.1.0", + "@alilc/b6-compact-legao-builtin": "1.x", + "antd": "3.x", + "@alilc/b6-util-mocks": "1.x" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/packages/rax-renderer/demo/miniapp/pages/index.acss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css similarity index 100% rename from packages/rax-renderer/demo/miniapp/pages/index.acss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx new file mode 100644 index 0000000000..2945a9d8f5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/pages/Aaaa/index.jsx @@ -0,0 +1,118 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Page } from '@alilc/b6-page'; + +import { Text } from '@alilc/b6-text'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Aaaa$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + description: 'URL参数', + options: function () { + return { + uri: '', + }; + }.bind(_this), + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div title="" backgroundColor="#fff" textColor="#333" style={{}}> + <Text + content="欢迎使用 BuildSuccess!sadsad" + style={{}} + fieldId="text_kp6ci11t" + /> + </div> + ); + } +} + +export default Aaaa$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..76643a07f3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Aaaa from '@/pages/Aaaa'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/aaaa', + component: Aaaa, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..868d471106 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/src/utils.js @@ -0,0 +1,61 @@ +import legaoBuiltin from '@alilc/b6-compact-legao-builtin'; + +import { message, Modal as modal } from 'antd'; + +import { mocks } from '@alilc/b6-util-mocks'; + +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default { + legaoBuiltin, + + message, + + mocks, + + modal, +}; diff --git a/modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/schema.json5 new file mode 100644 index 0000000000..76e8248cbb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2-utils-name-alias/schema.json5 @@ -0,0 +1,123 @@ +{ + version: '1.0.0', + componentsMap: [ + { + package: '@alilc/b6-page', + version: '^0.1.0', + componentName: 'Page', + destructuring: true, + exportName: 'Page', + }, + { + package: '@alilc/b6-text', + version: '^0.1.0', + componentName: 'Text', + destructuring: true, + exportName: 'Text', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_ockp6ci0hm1', + props: { + title: '', + backgroundColor: '#fff', + textColor: '#333', + style: {}, + }, + fileName: 'aaaa', + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + description: 'URL参数', + options: { + uri: '', + }, + }, + ], + }, + children: [ + { + componentName: 'Text', + id: 'node_ockp6ci0hm2', + props: { + content: '欢迎使用 BuildSuccess!sadsad', + style: {}, + fieldId: 'text_kp6ci11t', + }, + }, + ], + meta: { + router: '/aaaa', + }, + methodsModule: { + type: 'JSModule', + compiled: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n value: true\n});\nexports.helloPage = helloPage;\n\n/**\n * Private, and can be re-used functions\n * Actions panel help documentation:\n * @see https://yuque.antfin.com/docs/share/89ca7965-6387-4e3a-9964-81929ed48f1e\n */\nfunction printLog(obj) {\n console.info(obj);\n}\n/**\n * page function\n */\n\n\nfunction helloPage() {\n console.log(\'hello page\');\n}', + source: "/**\n * Private, and can be re-used functions\n * Actions panel help documentation:\n * @see https://yuque.antfin.com/docs/share/89ca7965-6387-4e3a-9964-81929ed48f1e\n */\nfunction printLog(obj) {\n console.info(obj);\n}\n\n/**\n * page function\n */\nexport function helloPage() {\n console.log('hello page');\n}", + }, + }, + ], + i18n: {}, + utils: [ + { + name: 'legaoBuiltin', + type: 'npm', + content: { + exportName: 'legaoBuiltin', + package: '@alilc/b6-compact-legao-builtin', + version: '1.x', + }, + }, + { + name: 'message', + type: 'npm', + content: { + package: 'antd', + version: '3.x', + destructuring: true, + exportName: 'message', + }, + }, + { + name: 'mocks', + type: 'npm', + content: { + package: '@alilc/b6-util-mocks', + version: '1.x', + exportName: 'mocks', + destructuring: true, + }, + }, + { + name: 'modal', + type: 'npm', + content: { + package: 'antd', + version: '3.x', + destructuring: true, + exportName: 'Modal', + }, + }, + ], + constants: {}, + dataSource: { + list: [], + }, + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'root', + miniAppBuildType: 'runtime', + }, + meta: { + name: 'jinyuan-test2', + git_group: 'b6', + project_name: 'jinyuan-test2', + description: '瑾源测试', + spma: 'spmademo', + creator: '张三', + }, +} diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/package.json new file mode 100644 index 0000000000..03820c1574 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/package.json @@ -0,0 +1,48 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..e8cb58e640 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/i18n.js @@ -0,0 +1,86 @@ +const i18nConfig = { + 'zh-CN': { + 'i18n-jwg27yo4': '你好', + 'i18n-jwg27yo3': '中国', + }, + 'en-US': { + 'i18n-jwg27yo4': 'Hello', + 'i18n-jwg27yo3': 'China', + }, +}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..080c4245b6 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,129 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidMount() { + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item + label={__$$eval(() => this.i18n('i18n-jwg27yo4'))} + name="name" + initValue="李雷" + > + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + <Button + type="primary" + style={{ margin: '0 5px 0 5px' }} + htmlType="submit" + > + 提交 + </Button> + <Button + type="normal" + style={{ margin: '0 5px 0 5px' }} + htmlType="reset" + > + 重置 + </Button> + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..47a0f2d417 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo2/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo2/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo2/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/schema.json5 new file mode 100644 index 0000000000..2228212067 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo2/schema.json5 @@ -0,0 +1,256 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button" + }, + { + "componentName": "Button.Group", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "Input", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Input" + }, + { + "componentName": "Form", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form" + }, + { + "componentName": "Form.Item", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form", + "subName": "Item" + }, + { + "componentName": "NumberPicker", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "NumberPicker" + }, + { + "componentName": "Select", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Select" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node$1", + "meta": { + "title": "测试", + "router": "/" + }, + "props": { + "ref": "outterView", + "autoLoading": true + }, + "fileName": "test", + "state": { + "text": "outter" + }, + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() { console.log('componentDidMount'); }" + } + }, + "children": [ + { + "componentName": "Form", + "id": "node$2", + "props": { + "labelCol": { + "type": "JSExpression", + "value": "this.state.colNum" + }, + "style": {}, + "ref": "testForm" + }, + "children": [ + { + "componentName": "Form.Item", + "id": "node$3", + "props": { + "label": { + type: 'JSExpression', + value: 'this.i18n("i18n-jwg27yo4")', + }, + "name": "name", + "initValue": "李雷" + }, + "children": [ + { + "componentName": "Input", + "id": "node$4", + "props": { + "placeholder": "请输入", + "size": "medium", + "style": { + "width": 320 + } + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$5", + "props": { + "label": "年龄:", + "name": "age", + "initValue": "22" + }, + "children": [ + { + "componentName": "NumberPicker", + "id": "node$6", + "props": { + "size": "medium", + "type": "normal" + } + } + ] + }, + { + "componentName": "Form.Item", + "id": "node$7", + "props": { + "label": "职业:", + "name": "profession" + }, + "children": [ + { + "componentName": "Select", + "id": "node$8", + "props": { + "dataSource": [ + { + "label": "教师", + "value": "t" + }, + { + "label": "医生", + "value": "d" + }, + { + "label": "歌手", + "value": "s" + } + ] + } + } + ] + }, + { + "componentName": "Div", + "id": "node$9", + "props": { + "style": { + "textAlign": "center" + } + }, + "children": [ + { + "componentName": "Button.Group", + "id": "node$a", + "props": {}, + "children": [ + { + "componentName": "Button", + "id": "node$b", + "props": { + "type": "primary", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "submit" + }, + "children": [ + "提交" + ] + }, + { + "componentName": "Button", + "id": "node$d", + "props": { + "type": "normal", + "style": { + "margin": "0 5px 0 5px" + }, + "htmlType": "reset" + }, + "children": [ + "重置" + ] + } + ] + } + ] + } + ] + } + ] + } + ], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.xxx.com" + }, + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "中国" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "China" + } + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + } + }, + "theme": { + "package": "@alife/theme-fusion", + "version": "^0.1.0", + "primary": "#ff9966" + } + }, + "meta": { + "name": "demo应用", + "git_group": "appGroup", + "project_name": "app_demo", + "description": "这是一个测试应用", + "spma": "spa23d", + "creator": "月飞" + } +} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/package.json new file mode 100644 index 0000000000..03820c1574 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/package.json @@ -0,0 +1,48 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..e8cb58e640 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/i18n.js @@ -0,0 +1,86 @@ +const i18nConfig = { + 'zh-CN': { + 'i18n-jwg27yo4': '你好', + 'i18n-jwg27yo3': '中国', + }, + 'en-US': { + 'i18n-jwg27yo4': 'Hello', + 'i18n-jwg27yo3': 'China', + }, +}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.less b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.less rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..2f0a6efa9a --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,107 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import Super, { + Button, + Input as CustomInput, + Form, + NumberPicker, + Select, + SearchTable as SearchTableExport, +} from '@alifd/next'; + +import SuperOther from '@alifd/next'; + +import '@alifd/next/lib/super/style'; + +import '@alifd/next/lib/button/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/search-table/style'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const SuperSub = Super.Sub; + +const SelectOption = Select.Option; + +const SearchTable = SearchTableExport.default; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + <Super title={__$$eval(() => this.state.title)} /> + <SuperSub /> + <SuperOther /> + <Button /> + <Button.Group /> + <CustomInput /> + <Form.Item /> + <NumberPicker /> + <SelectOption /> + <SearchTable /> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..47a0f2d417 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo3/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo3/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo3/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/schema.json5 new file mode 100644 index 0000000000..173b793e13 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo3/schema.json5 @@ -0,0 +1,159 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "componentName": "Super", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": false, + "exportName": "Super" + }, + { + "componentName": "SuperOther", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": false, + "exportName": "Super" + }, + { + "componentName": "SuperSub", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": false, + "exportName": "Super", + "subName": "Sub", + }, + { + "componentName": "Button", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button" + }, + { + "componentName": "SearchTable", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "SearchTable", + "subName": "default", + }, + { + "componentName": "Button.Group", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Button", + "subName": "Group" + }, + { + "componentName": "CustomInput", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Input" + }, + { + "componentName": "Form", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form" + }, + { + "componentName": "Form.Item", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Form", + "subName": "Item" + }, + { + "componentName": "NumberPicker", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "NumberPicker" + }, + { + "componentName": "SelectOption", + "package": "@alifd/next", + "version": "1.19.18", + "destructuring": true, + "exportName": "Select", + "subName": "Option" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node$1", + "meta": { + "title": "测试", + "router": "/" + }, + "fileName": "test", + "children": [ + { + "componentName": "Super", + "props": { + "title": { + "type":"variable", + "value":"标题", + "variable":"this.state.title" + } + } + }, + { "componentName": "SuperSub" }, + { "componentName": "SuperOther" }, + { "componentName": "Button" }, + { "componentName": "Button.Group" }, + { "componentName": "CustomInput" }, + { "componentName": "Form.Item" }, + { "componentName": "NumberPicker" }, + { "componentName": "SelectOption" }, + { "componentName": "SearchTable" }, + ] + } + ], + "constants": { + "ENV": "prod", + "DOMAIN": "xxx.xxx.com" + }, + "i18n": { + "zh-CN": { + "i18n-jwg27yo4": "你好", + "i18n-jwg27yo3": "中国" + }, + "en-US": { + "i18n-jwg27yo4": "Hello", + "i18n-jwg27yo3": "China" + } + }, + "css": "body {font-size: 12px;} .table { width: 100px;}", + "config": { + "sdkVersion": "1.0.3", + "historyMode": "hash", + "targetRootID": "J_Container", + "layout": { + "componentName": "BasicLayout", + "props": { + "logo": "...", + "name": "测试网站" + } + }, + "theme": { + "package": "@alife/theme-fusion", + "version": "^0.1.0", + "primary": "#ff9966" + } + }, + "meta": { + "name": "demo应用", + "git_group": "appGroup", + "project_name": "app_demo", + "description": "这是一个测试应用", + "spma": "spa23d", + "creator": "月飞" + } +} diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/package.json new file mode 100644 index 0000000000..6190fc0238 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/package.json @@ -0,0 +1,50 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0", + "@alife/container": "^1.0.0", + "@alife/mc-assets-1935": "0.1.9" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..066114aeeb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.css @@ -0,0 +1,8 @@ +body { + font-size: 12px; +} + +.botton { + width: 100px; + color: #ff00ff; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..6400d7445b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,292 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, + Text as NextText, +} from '@alife/container/lib/index.js'; + +import { AliSearchTable as AliSearchTableExport } from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +const AliSearchTable = AliSearchTableExport.default; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { fetch: __$$createFetchRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter', isShowDialog: false }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + type: 'fetch', + isInit: function () { + return true; + }.bind(_this), + options: function () { + return { + params: {}, + method: 'GET', + isCors: true, + timeout: 5000, + headers: {}, + uri: 'https://mocks.xxx.com/mock/jjpin/user/list', + }; + }.bind(_this), + id: 'users', + }, + ], + }; + } + + componentWillUnmount() { + console.log('will umount'); + } + + componentDidUpdate(prevProps, prevState, snapshot) { + console.log(this.state); + } + + testFunc() { + console.log('test func'); + } + + onClick() { + this.setState({ + isShowDialog: true, + }); + } + + closeDialog() { + this.setState({ + isShowDialog: false, + }); + } + + onSearch(values) { + console.log('search form:', values); + console.log(this.dataSourceMap); + this.dataSourceMap['users'].load(values); + } + + onClear() { + console.log('form reset'); + this.setState({ + isShowDialog: true, + }); + } + + onPageChange(page, pageSize) { + console.log(`page: ${page}, pageSize: ${pageSize}`); + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('did mount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <NextPage + columns={12} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={ + <NextP + wrap={true} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <NextText type="h5">员工列表</NextText> + </NextP> + } + headerTest={[]} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + colSpan={12} + rowSpan={1} + childTotalColumns="1fr" + > + <NextBlockCell + title="" + primaryKey="732" + prefix="next-" + placeholderStyle={{ height: '100%' }} + colSpan={1} + rowSpan={1} + > + <NextP + wrap={true} + type="body2" + textSpacing={true} + verAlign="center" + align="flex-start" + flex={true} + > + <AliSearchTable + dataSource={__$$eval(() => this.state.users.data)} + rowKey="workid" + columns={[ + { title: '花名', dataIndex: 'cname' }, + { title: 'user_id', dataIndex: 'workid' }, + { title: '部门', dataIndex: 'dep' }, + ]} + searchItems={[ + { label: '姓名', name: 'cname' }, + { label: '部门', name: 'dep' }, + ]} + onSearch={function () { + return this.onSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onClear={function () { + return this.onClear.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + pagination={{ + defaultPageSize: '', + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + showSizeChanger: true, + }} + /> + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + colSpan={12} + rowSpan={1} + childTotalColumns={1} + > + <NextBlockCell + title="" + primaryKey="472" + prefix="next-" + placeholderStyle={{ height: '100%' }} + colSpan={1} + rowSpan={1} + /> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..6832d13682 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo4/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo4/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo4/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/schema.json5 new file mode 100644 index 0000000000..ca97204e9c --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo4/schema.json5 @@ -0,0 +1,353 @@ +{ + "version": "1.0.0", + "componentsMap": [ + { + "package": "@alife/mc-assets-1935", + "version": "0.1.9", + "exportName": "AliSearchTable", + "main": "build/lowcode/index.js", + "subName": "default", + "destructuring": true, + "componentName": "AliSearchTable" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "P", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextP" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Block", + "main": "lib/index.js", + "destructuring": true, + "subName": "Cell", + "componentName": "NextBlockCell" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Block", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextBlock" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Text", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextText" + }, + { + "package": "@alife/container", + "version": "^1.0.0", + "exportName": "Page", + "main": "lib/index.js", + "destructuring": true, + "subName": "", + "componentName": "NextPage" + } + ], + "componentsTree": [ + { + "componentName": "Page", + "id": "node_dockcviv8fo1", + "props": { + "ref": "outterView", + "style": { + "height": "100%" + } + }, + "fileName": "test", + "dataSource": { + "list": [ + { + "type": "fetch", + "isInit": true, + "options": { + "params": {}, + "method": "GET", + "isCors": true, + "timeout": 5000, + "headers": {}, + "uri": "https://mocks.xxx.com/mock/jjpin/user/list" + }, + "id": "users" + } + ] + }, + "css": "body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}", + "lifeCycles": { + "componentDidMount": { + "type": "JSFunction", + "value": "function() {\n console.log('did mount');\n }" + }, + "componentWillUnmount": { + "type": "JSFunction", + "value": "function() {\n console.log('will umount');\n }" + }, + "componentDidUpdate": { + "type": "JSFunction", + "value": "function(prevProps, prevState, snapshot) {\n console.log(this.state);\n }" + } + }, + "methods": { + "testFunc": { + "type": "JSFunction", + "value": "function() {\n console.log('test func');\n }" + }, + "onClick": { + "type": "JSFunction", + "value": "function() {\n this.setState({\n isShowDialog: true\n })\n }" + }, + "closeDialog": { + "type": "JSFunction", + "value": "function() {\n this.setState({\n isShowDialog: false\n })\n }" + }, + "onSearch": { + "type": "JSFunction", + "value": "function(values) {\n console.log('search form:', values)\n console.log(this.dataSourceMap);\n this.dataSourceMap['users'].load(values)\n }" + }, + "onClear": { + "type": "JSFunction", + "value": "function() {\n console.log('form reset')\n this.setState({\n isShowDialog: true\n })\n }" + }, + "onPageChange": { + "type": "JSFunction", + "value": "function(page, pageSize) {\n console.log(`page: ${page}, pageSize: ${pageSize}`)\n }" + } + }, + "state": { + "text": "outter", + "isShowDialog": false + }, + "children": [ + { + "componentName": "NextPage", + "id": "node_ockkgjwi8z1", + "props": { + "columns": 12, + "placeholderStyle": { + "gridRowEnd": "span 1", + "gridColumnEnd": "span 12" + }, + "placeholder": "页面主体内容:拖拽Block布局组件到这里", + "header": { + "type": "JSSlot", + "value": [ + { + "componentName": "NextP", + "id": "node_ockkgjwi8zn", + "props": { + "wrap": true, + "type": "body2", + "verAlign": "middle", + "textSpacing": true, + "align": "left", + "flex": true + }, + "children": [ + { + "componentName": "NextText", + "id": "node_ockkgjwi8zo", + "props": { + "type": "h5", + "children": "员工列表" + } + } + ] + } + ], + "title": "header" + }, + "headerTest": { + "type": "JSSlot", + "value": [], + "title": "header" + }, + "headerProps": { + "background": "surface" + }, + "footer": { + "type": "JSSlot", + "title": "footer" + }, + "minHeight": "100vh" + }, + "children": [ + { + "componentName": "NextBlock", + "id": "node_ockkgjwi8z2", + "props": { + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "noPadding": false, + "noBorder": false, + "background": "surface", + "colSpan": 12, + "rowSpan": 1, + "childTotalColumns": "1fr" + }, + "title": "分区", + "children": [ + { + "componentName": "NextBlockCell", + "id": "node_ockkgjwi8z3", + "props": { + "title": "", + "primaryKey": "732", + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "colSpan": 1, + "rowSpan": 1 + }, + "children": [ + { + "componentName": "NextP", + "id": "node_ockkgjwi8zu", + "props": { + "wrap": true, + "type": "body2", + "textSpacing": true, + "verAlign": "center", + "align": "flex-start", + "flex": true + }, + "children": [ + { + "componentName": "AliSearchTable", + "id": "node_ockkgjwi8zv", + "props": { + "dataSource": { + "type": "JSExpression", + "value": "this.state.users.data" + }, + "rowKey": "workid", + "columns": [ + { + "title": "花名", + "dataIndex": "cname" + }, + { + "title": "user_id", + "dataIndex": "workid" + }, + { + "title": "部门", + "dataIndex": "dep" + } + ], + "searchItems": [ + { + "label": "姓名", + "name": "cname" + }, + { + "label": "部门", + "name": "dep" + } + ], + "onSearch": { + "type": "JSFunction", + "value": "function(){ return this.onSearch.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "onClear": { + "type": "JSFunction", + "value": "function(){ return this.onClear.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "pagination": { + "defaultPageSize": "", + "onPageChange": { + "type": "JSFunction", + "value": "function(){ return this.onPageChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }" + }, + "showSizeChanger": true + } + } + } + ] + } + ] + } + ] + } + ] + }, + { + "componentName": "NextPage", + "id": "node_ockm4jxd6313", + "props": { + "columns": 12, + "headerDivider": true, + "placeholderStyle": { + "gridRowEnd": "span 1", + "gridColumnEnd": "span 12" + }, + "placeholder": "页面主体内容:拖拽Block布局组件到这里", + "header": { + "type": "JSSlot", + "title": "header" + }, + "headerProps": { + "background": "surface" + }, + "footer": { + "type": "JSSlot", + "title": "footer" + }, + "minHeight": "100vh" + }, + "title": "页面", + "children": [ + { + "componentName": "NextBlock", + "id": "node_ockm4jxd6314", + "props": { + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "noPadding": false, + "noBorder": false, + "background": "surface", + "colSpan": 12, + "rowSpan": 1, + "childTotalColumns": 1 + }, + "title": "区块", + "children": [ + { + "componentName": "NextBlockCell", + "id": "node_ockm4jxd6315", + "props": { + "title": "", + "primaryKey": "472", + "prefix": "next-", + "placeholderStyle": { + "height": "100%" + }, + "colSpan": 1, + "rowSpan": 1 + } + } + ] + } + ] + } + ] + } + ], + "i18n": {} +} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/package.json new file mode 100644 index 0000000000..b31603b3a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/package.json @@ -0,0 +1,51 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alife/container": "0.3.7", + "@alilc/antd-lowcode": "0.5.4", + "@alife/mc-assets-1935": "0.1.16" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..066114aeeb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.css @@ -0,0 +1,8 @@ +body { + font-size: 12px; +} + +.botton { + width: 100px; + color: #ff00ff; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..7427f164d9 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,389 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import { + Card, + Space, + Typography, + Select, + Button, + Modal, + Form, + InputNumber, + Input, +} from '@alilc/antd-lowcode/dist/antd-lowcode.esm.js'; + +import { AliAutoSearchTable } from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + name: 'nongzhou', + gateways: [], + selectedGateway: null, + records: [], + modalVisible: false, + }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentWillUnmount() { + /* ... */ + } + + componentDidUpdate() { + /* ... */ + } + + onChange() { + /* ... */ + } + + getActions() { + /* ... */ + } + + onCreateOrder() { + /* ... */ + } + + onCancelModal() { + /* ... */ + } + + onConfirmCreateOrder() { + /* ... */ + } + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + style={{ cursor: 'pointer' }} + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + > + <Card title=""> + <Space size={0} align="center" direction="horizontal"> + <Typography.Text>所在网关:</Typography.Text> + <Select + style={{ + marginTop: '16px', + marginRight: '16px', + marginBottom: '16px', + marginLeft: '16px', + width: '400px', + display: 'inline-block', + }} + options={__$$eval(() => this.state.gateways)} + mode="single" + defaultValue={['auto-edd-uniproxy']} + labelInValue={true} + showSearch={true} + allowClear={false} + placeholder="请选取网关" + showArrow={true} + loading={false} + tokenSeparators={[]} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'onChange', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: false }, + { name: 'onSelect', disabled: false }, + { name: 'onDropdownVisibleChange', disabled: false }, + ], + }} + onChange={function () { + this.onChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Space> + <Button + type="primary" + style={{ + display: 'block', + marginTop: '20px', + marginBottom: '20px', + }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCreateOrder', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onCreateOrder.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 创建发布单 + </Button> + <Modal + title="创建发布单" + visible={__$$eval(() => this.state.modalVisible)} + footer="" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onCancelModal.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + zIndex={2000} + > + <Form + labelCol={{ span: 6 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onConfirmCreateOrder.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onConfirmCreateOrder', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label="发布批次"> + <InputNumber value={3} min={1} /> + </Form.Item> + <Form.Item label="批次间隔时间"> + <InputNumber value={3} /> + </Form.Item> + <Form.Item label="备注 "> + <Input.TextArea rows={3} placeholder="请输入" /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: 6 }} + style={{ + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + display: 'flex', + }} + labelAlign="right" + > + <Button type="primary" htmlType="submit"> + 提交 + </Button> + <Button + style={{ marginLeft: 20 }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onCancelModal.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 取消 + </Button> + </Form.Item> + </Form> + </Modal> + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.records)} + columns={[ + { + title: '发布名称', + dataIndex: 'order_name', + key: 'name', + }, + { + title: '类型', + dataIndex: 'order_type_desc', + key: 'age', + }, + { + title: '发布状态', + dataIndex: 'order_status_desc', + key: 'address', + }, + { title: '发布人', dataIndex: 'creator_name' }, + { title: '当前批次/总批次', dataIndex: 'cur_batch_no' }, + { + title: '发布机器/总机器', + dataIndex: 'pubblish_ip_finish_num', + }, + { title: '发布时间', dataIndex: 'publish_id' }, + ]} + actions={__$$eval(() => this.actions || [])} + getActions={function () { + return this.getActions.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Card> + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..6832d13682 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo5/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo5/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo5/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/schema.json5 new file mode 100644 index 0000000000..b59b97e763 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo5/schema.json5 @@ -0,0 +1,677 @@ +{ + version: '1.0.0', + componentsMap: [ + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Typography', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Text', + componentName: 'Typography.Text', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Select', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Select', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Space', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Space', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Button', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Button', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'InputNumber', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'InputNumber', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Item', + componentName: 'Form.Item', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Input', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'TextArea', + componentName: 'Input.TextArea', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Form', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Modal', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Modal', + }, + { + package: '@alife/mc-assets-1935', + version: '0.1.16', + exportName: 'AliAutoSearchTable', + main: 'build/lowcode/index.js', + destructuring: true, + subName: 'default', + componentName: 'AliAutoSearchTableDefault', + }, + { + package: '@alilc/antd-lowcode', + version: '0.5.4', + exportName: 'Card', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Card', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'P', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextP', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: 'Cell', + componentName: 'NextBlockCell', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextBlock', + }, + { + devMode: 'lowcode', + componentName: 'Slot', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Page', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextPage', + }, + { + devMode: 'lowcode', + componentName: 'Page', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_dockcviv8fo1', + props: { + ref: 'outterView', + style: { + height: '100%', + }, + }, + fileName: 'test', + dataSource: { + list: [], + }, + css: 'body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}', + lifeCycles: { + componentDidMount: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + componentWillUnmount: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + componentDidUpdate: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + }, + methods: { + onChange: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + getActions: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + onCreateOrder: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + onCancelModal: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + onConfirmCreateOrder: { + type: 'JSFunction', + value: 'function() { /* ... */ }', + }, + }, + state: { + name: 'nongzhou', + gateways: [], + selectedGateway: null, + records: [], + modalVisible: false, + }, + children: [ + { + componentName: 'NextPage', + id: 'node_ocknqx3esma', + props: { + columns: 12, + headerDivider: true, + placeholderStyle: { + gridRowEnd: 'span 1', + gridColumnEnd: 'span 12', + }, + placeholder: '页面主体内容:拖拽Block布局组件到这里', + header: { + type: 'JSSlot', + title: 'header', + }, + headerProps: { + background: 'surface', + }, + footer: { + type: 'JSSlot', + title: 'footer', + }, + minHeight: '100vh', + style: { + cursor: 'pointer', + }, + }, + title: '页面', + children: [ + { + componentName: 'NextBlock', + id: 'node_ocknqx3esmb', + props: { + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + noPadding: false, + noBorder: false, + background: 'surface', + layoutmode: 'O', + colSpan: 12, + rowSpan: 1, + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ocknqx3esmc', + props: { + title: '', + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + layoutmode: 'O', + childTotalColumns: 12, + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + children: [ + { + componentName: 'NextP', + id: 'node_ocknqx3esm1j', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + full: true, + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'Card', + id: 'node_ocknqx3esm1k', + props: { + title: '', + }, + children: [ + { + componentName: 'Space', + id: 'node_ocknqx3esm1n', + props: { + size: 0, + align: 'center', + direction: 'horizontal', + }, + children: [ + { + componentName: 'Typography.Text', + id: 'node_ocknqx3esm1l', + props: { + children: '所在网关:', + }, + }, + { + componentName: 'Select', + id: 'node_ocknqx3esm1m', + props: { + style: { + marginTop: '16px', + marginRight: '16px', + marginBottom: '16px', + marginLeft: '16px', + width: '400px', + display: 'inline-block', + }, + options: { + type: 'JSExpression', + value: 'this.state.gateways', + }, + mode: 'single', + defaultValue: ['auto-edd-uniproxy'], + labelInValue: true, + showSearch: true, + allowClear: false, + placeholder: '请选取网关', + showArrow: true, + loading: false, + tokenSeparators: [], + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'onChange', + }, + ], + eventList: [ + { + name: 'onBlur', + disabled: false, + }, + { + name: 'onChange', + disabled: true, + }, + { + name: 'onDeselect', + disabled: false, + }, + { + name: 'onFocus', + disabled: false, + }, + { + name: 'onInputKeyDown', + disabled: false, + }, + { + name: 'onMouseEnter', + disabled: false, + }, + { + name: 'onMouseLeave', + disabled: false, + }, + { + name: 'onPopupScroll', + disabled: false, + }, + { + name: 'onSearch', + disabled: false, + }, + { + name: 'onSelect', + disabled: false, + }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }, + onChange: { + type: 'JSFunction', + value: 'function(){this.onChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + { + componentName: 'Button', + id: 'node_ockntwgdsn7', + props: { + type: 'primary', + children: '创建发布单', + style: { + display: 'block', + marginTop: '20px', + marginBottom: '20px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCreateOrder', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onCreateOrder.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + { + componentName: 'Modal', + id: 'node_ockntx4eo9p', + props: { + title: '创建发布单', + visible: { + type: 'JSExpression', + value: 'this.state.modalVisible', + }, + footer: '', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [ + { + name: 'onCancel', + disabled: true, + }, + { + name: 'onOk', + disabled: false, + }, + ], + }, + onCancel: { + type: 'JSFunction', + value: 'function(){this.onCancelModal.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + zIndex: 2000, + }, + hidden: true, + children: [ + { + componentName: 'Form', + id: 'node_ockntx4eo9s', + props: { + labelCol: { + span: 6, + }, + wrapperCol: { + span: 14, + }, + onFinish: { + type: 'JSFunction', + value: 'function(){this.onConfirmCreateOrder.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onConfirmCreateOrder', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: true, + }, + { + name: 'onFinishFailed', + disabled: false, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: false, + }, + ], + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ockntx4eo91k', + props: { + label: '发布批次', + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockntx4eo91l', + props: { + value: 3, + min: 1, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockntx4eo91r', + props: { + label: '批次间隔时间', + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockntx4eo91s', + props: { + value: 3, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockntx4eo91y', + props: { + label: '备注 ', + }, + children: [ + { + componentName: 'Input.TextArea', + id: 'node_ockntx4eo91z', + props: { + rows: 3, + placeholder: '请输入', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockntx4eo9v', + props: { + wrapperCol: { + offset: 6, + }, + style: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + display: 'flex', + }, + labelAlign: 'right', + }, + children: [ + { + componentName: 'Button', + id: 'node_ockntx4eo9w', + props: { + type: 'primary', + children: '提交', + htmlType: 'submit', + }, + }, + { + componentName: 'Button', + id: 'node_ockntx4eo9x', + props: { + style: { + marginLeft: 20, + }, + children: '取消', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onCancelModal', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onCancelModal.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'AliAutoSearchTableDefault', + id: 'node_ocknqx3esm1q', + props: { + rowKey: 'key', + dataSource: { + type: 'JSExpression', + value: 'this.state.records', + }, + columns: [ + { + title: '发布名称', + dataIndex: 'order_name', + key: 'name', + }, + { + title: '类型', + dataIndex: 'order_type_desc', + key: 'age', + }, + { + title: '发布状态', + dataIndex: 'order_status_desc', + key: 'address', + }, + { + title: '发布人', + dataIndex: 'creator_name', + }, + { + title: '当前批次/总批次', + dataIndex: 'cur_batch_no', + }, + { + title: '发布机器/总机器', + dataIndex: 'pubblish_ip_finish_num', + }, + { + title: '发布时间', + dataIndex: 'publish_id', + }, + ], + actions: { + type: 'JSExpression', + value: 'this.actions || []', + }, + getActions: { + type: 'JSFunction', + value: 'function(){ return this.getActions.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + i18n: {}, +} diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json new file mode 100644 index 0000000000..d18604922d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/package.json @@ -0,0 +1,50 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-url-params-handler": "^1.0.0", + "@alilc/lowcode-datasource-fetch-handler": "^1.0.0", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..91198f9044 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = { ENV: 'prod', DOMAIN: 'xxx.xxx.com' }; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..ed7204b4a3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/global.scss @@ -0,0 +1,13 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} + +body { + font-size: 12px; +} +.table { + width: 100px; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/packages/rax-simulator-renderer/src/utils/get-closest-node-instance.ts b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css similarity index 100% rename from packages/rax-simulator-renderer/src/utils/get-closest-node-instance.ts rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.css diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..515940c334 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,205 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Form, Input, NumberPicker, Select, Button } from '@alifd/next'; + +import { createUrlParamsHandler as __$$createUrlParamsRequestHandler } from '@alilc/lowcode-datasource-url-params-handler'; + +import { createFetchHandler as __$$createFetchRequestHandler } from '@alilc/lowcode-datasource-fetch-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/form/style'; + +import '@alifd/next/lib/input/style'; + +import '@alifd/next/lib/number-picker/style'; + +import '@alifd/next/lib/select/style'; + +import '@alifd/next/lib/button/style'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Test$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { + urlParams: __$$createUrlParamsRequestHandler(window.location.search), + fetch: __$$createFetchRequestHandler(), + }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { text: 'outter' }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'urlParams', + type: 'urlParams', + isInit: function () { + return undefined; + }.bind(_this), + options: function () { + return undefined; + }.bind(_this), + }, + { + id: 'user', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + { + id: 'orders', + type: 'fetch', + options: function () { + return { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }; + }.bind(_this), + dataHandler: function (response) { + if (!response.data.success) { + throw new Error(response.data.message); + } + return response.data.data.result; + }, + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + dataHandler: function (dataMap) { + console.info('All datasources loaded:', dataMap); + }, + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + + console.log('componentDidMount'); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div ref={this._refsManager.linkRef('outterView')} autoLoading={true}> + <Form + labelCol={__$$eval(() => this.state.colNum)} + style={{}} + ref={this._refsManager.linkRef('testForm')} + > + <Form.Item label="姓名:" name="name" initValue="李雷"> + <Input placeholder="请输入" size="medium" style={{ width: 320 }} /> + </Form.Item> + <Form.Item label="年龄:" name="age" initValue="22"> + <NumberPicker size="medium" type="normal" /> + </Form.Item> + <Form.Item label="职业:" name="profession"> + <Select + dataSource={[ + { label: '教师', value: 't' }, + { label: '医生', value: 'd' }, + { label: '歌手', value: 's' }, + ]} + /> + </Form.Item> + <div style={{ textAlign: 'center' }}> + <Button.Group> + {__$$evalArray(() => ['a', 'b', 'c']).map((item, index) => + ((__$$context) => + !!false && ( + <Button type="primary" style={{ margin: '0 5px 0 5px' }}> + {__$$eval(() => item)} + </Button> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Button.Group> + </div> + </Form> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..47a0f2d417 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '/', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo6-literal-condition/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/schema.json5 new file mode 100644 index 0000000000..5b6776c1ee --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo6-literal-condition/schema.json5 @@ -0,0 +1,273 @@ +{ + version: '1.0.0', + componentsMap: [ + { + componentName: 'Button', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Button', + }, + { + componentName: 'Button.Group', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Button', + subName: 'Group', + }, + { + componentName: 'Input', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Input', + }, + { + componentName: 'Form', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Form', + }, + { + componentName: 'Form.Item', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Form', + subName: 'Item', + }, + { + componentName: 'NumberPicker', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'NumberPicker', + }, + { + componentName: 'Select', + package: '@alifd/next', + version: '1.19.18', + destructuring: true, + exportName: 'Select', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node$1', + meta: { + title: '测试', + router: '/', + }, + props: { + ref: 'outterView', + autoLoading: true, + }, + fileName: 'test', + state: { + text: 'outter', + }, + lifeCycles: { + componentDidMount: { + type: 'JSFunction', + value: "function() { console.log('componentDidMount'); }", + }, + }, + dataSource: { + list: [ + { + id: 'urlParams', + type: 'urlParams', + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/user + { + id: 'user', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/user', + isSync: true, + }, + dataHandler: { + type: 'JSFunction', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data;\n}', + }, + }, + // 示例数据源:https://shs.xxx.com/mock/1458/demo/orders + { + id: 'orders', + type: 'fetch', + options: { + method: 'GET', + uri: 'https://shs.xxx.com/mock/1458/demo/orders', + isSync: true, + }, + dataHandler: { + type: 'JSFunction', + value: 'function (response) {\nif (!response.data.success){\n throw new Error(response.data.message);\n }\n return response.data.data.result;\n}', + }, + }, + ], + dataHandler: { + type: 'JSFunction', + value: 'function (dataMap) {\n console.info("All datasources loaded:", dataMap);\n}', + }, + }, + children: [ + { + componentName: 'Form', + id: 'node$2', + props: { + labelCol: { + type: 'JSExpression', + value: 'this.state.colNum', + }, + style: {}, + ref: 'testForm', + }, + children: [ + { + componentName: 'Form.Item', + id: 'node$3', + props: { + label: '姓名:', + name: 'name', + initValue: '李雷', + }, + children: [ + { + componentName: 'Input', + id: 'node$4', + props: { + placeholder: '请输入', + size: 'medium', + style: { + width: 320, + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node$5', + props: { + label: '年龄:', + name: 'age', + initValue: '22', + }, + children: [ + { + componentName: 'NumberPicker', + id: 'node$6', + props: { + size: 'medium', + type: 'normal', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node$7', + props: { + label: '职业:', + name: 'profession', + }, + children: [ + { + componentName: 'Select', + id: 'node$8', + props: { + dataSource: [ + { + label: '教师', + value: 't', + }, + { + label: '医生', + value: 'd', + }, + { + label: '歌手', + value: 's', + }, + ], + }, + }, + ], + }, + { + componentName: 'Div', + id: 'node$9', + props: { + style: { + textAlign: 'center', + }, + }, + children: [ + { + componentName: 'Button.Group', + id: 'node$a', + props: {}, + children: [ + { + componentName: 'Button', + id: 'node$b', + condition: false, + loop: ['a', 'b', 'c'], + props: { + type: 'primary', + style: { + margin: '0 5px 0 5px', + }, + }, + children: [ + { + type: 'JSExpression', + value: 'this.item', + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + constants: { + ENV: 'prod', + DOMAIN: 'xxx.xxx.com', + }, + css: 'body {font-size: 12px;} .table { width: 100px;}', + config: { + sdkVersion: '1.0.3', + historyMode: 'hash', + targetRootID: 'J_Container', + layout: { + componentName: 'BasicLayout', + props: { + logo: '...', + name: '测试网站', + }, + }, + theme: { + package: '@alife/theme-fusion', + version: '^0.1.0', + primary: '#ff9966', + }, + }, + meta: { + name: 'demo应用', + git_group: 'appGroup', + project_name: 'app_demo', + description: '这是一个测试应用', + spma: 'spa23d', + creator: '月飞', + }, +} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json new file mode 100644 index 0000000000..84141875ec --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/package.json @@ -0,0 +1,50 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode": "0.8.0", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..066114aeeb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.css @@ -0,0 +1,8 @@ +body { + font-size: 12px; +} + +.botton { + width: 100px; + color: #ff00ff; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..9e93a3ff6f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,1076 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Steps, + Form, + Input, + Checkbox, + Select, + DatePicker, + InputNumber, + Button, +} from '@alilc/antd-lowcode/dist/antd-lowcode.esm.js'; + +import { + Text as NextText, + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + books: [], + currentStep: 0, + isModifyDialogVisible: false, + isModifyStatus: false, + secondCommitText: '完成并提交', + thirdAuditText: '审核中', + thirdButtonText: '修改', + customerProjectInfo: { + id: null, + systemProjectName: null, + projectVersionTypeArray: null, + projectVersionType: null, + versionLine: 2, + expectedTime: null, + expectedNum: null, + projectModal: null, + displayWidth: null, + displayHeight: null, + displayInch: null, + displayDpi: null, + mainSoc: null, + cpuCoreNum: null, + instructions: null, + osVersion: null, + status: null, + }, + versionLinesArray: [ + { label: 'AmapAuto 485', value: 1 }, + { label: 'AmapAuto 505', value: 2 }, + ], + projectModalsArray: [ + { label: '车机', value: 1 }, + { label: '车镜', value: 2 }, + { label: '记录仪', value: 3 }, + { label: '其他', value: 4 }, + ], + osVersionsArray: [ + { label: '安卓5', value: 1 }, + { label: '安卓6', value: 2 }, + { label: '安卓7', value: 3 }, + { label: '安卓8', value: 4 }, + { label: '安卓9', value: 5 }, + { label: '安卓10', value: 6 }, + ], + instructionsArray: [ + { label: 'ARM64-V8', value: 'ARM64-V8' }, + { label: 'ARM32-V7', value: 'ARM32-V7' }, + { label: 'X86', value: 'X86' }, + { label: 'X64', value: 'X64' }, + ], + }; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + /*...*/ + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initUtils() { + /*...*/ + } + + onFinishFirst() { + /*...*/ + } + + onClickPreSecond() { + /*...*/ + } + + onFinishSecond() { + /*...*/ + } + + onClickModifyThird() { + /*...*/ + } + + onOkModifyDialogThird() { + //第三步 修改 对话框 确定 + + this.setState({ + currentStep: 0, + isModifyDialogVisible: false, + }); + } + + onCancelModifyDialogThird() { + //第三步 修改 对话框 取消 + + this.setState({ + isModifyDialogVisible: false, + }); + } + + onFinishFailed() {} + + onClickPreThird() { + // 第三步 上一步 + this.setState({ + currentStep: 1, + }); + } + + onClickFirstBack() { + // 第一步 返回按钮 + this.$router.push('/myProjectList'); + } + + onClickSecondBack() { + // 第二步 返回按钮 + this.$router.push('/myProjectList'); + } + + onClickThirdBack() { + // 第三步 返回按钮 + this.$router.push('/myProjectList'); + } + + onValuesChange(_, values) { + this.setState({ + customerProjectInfo: { + ...this.state.customerProjectInfo, + ...values, + }, + }); + } + + componentDidMount() {} + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="是否修改" + visible={__$$eval(() => this.state.isModifyDialogVisible)} + okText="确认" + okType="" + forceRender={false} + cancelText="取消" + zIndex={2000} + destroyOnClose={false} + confirmLoading={false} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onOk', + relatedEventName: 'onOkModifyDialogThird', + }, + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModifyDialogThird', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: true }, + ], + }} + onOk={function () { + this.onOkModifyDialogThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onCancel={function () { + this.onCancelModifyDialogThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + <NextText + type="inherit" + style={{ + fontStyle: 'normal', + textAlign: 'left', + display: 'block', + fontFamily: 'arial, helvetica, microsoft yahei', + fontWeight: 'normal', + }} + > + 修改将撤回此前填写的信息 + </NextText> + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + style={{}} + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + style={{ marginBottom: '24px' }} + > + <Steps current={__$$eval(() => this.state.currentStep)}> + <Steps.Step title="版本申请" description="" /> + <Steps.Step title="机器配置" subTitle="" description="" /> + <Steps.Step title="项目审批" description="" /> + </Steps> + </NextP> + {!!__$$eval(() => this.state.currentStep === 0) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinish={function () { + this.onFinishFirst.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishFirst', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: true }, + ], + }} + initialValues={__$$eval( + () => this.state.customerProjectInfo + )} + onValuesChange={function () { + this.onValuesChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + {!!false && ( + <Form.Item + label="" + style={{ width: '600px' }} + colon={false} + name="id" + > + <Input + placeholder="" + style={{ width: '600px' }} + bordered={false} + disabled={true} + /> + </Form.Item> + )} + <Form.Item + label="版本类型选择" + name="projectVersionTypeArray" + initialValue="" + labelAlign="left" + colon={false} + required={true} + style={{ flexDirection: 'column', width: '600px' }} + requiredobj={{ + required: true, + message: '请选择版本类型', + }} + > + <Checkbox.Group + options={[ + { label: '基础版本', value: '3' }, + { label: 'AR导航', value: '1' }, + { label: '货车导航', value: '2' }, + { label: 'UI定制', value: '4', disabled: false }, + ]} + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="版本线选择" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="versionLine" + requiredobj={{ required: true, message: '请选择版本线' }} + extra="" + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.versionLinesArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择版本线" + /> + </Form.Item> + <Form.Item + label="项目名称" + colon={false} + required={true} + style={{ display: 'flex' }} + labelAlign="left" + extra="" + name="systemProjectName" + requiredobj={{ + required: true, + message: '请按格式填写项目名称', + }} + typeobj={{ + type: 'string', + message: + '请输入项目名称,格式:公司简称-产品名称-版本类型', + }} + lenobj={{ + max: 100, + message: '项目名称不能超过100个字符', + }} + > + <Input + placeholder="公司简称-产品名称-版本类型" + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="预期交付时间" + style={{ width: '600px' }} + colon={false} + required={true} + name="expectedTime" + labelAlign="left" + requiredobj={{ + required: true, + message: '请填写预期交付时间', + }} + > + <DatePicker + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="预期出货量" + style={{ width: '600px' }} + required={true} + requiredobj={{ + required: true, + message: '请填写预期出货量', + }} + name="expectedNum" + labelAlign="left" + colon={false} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="单位(台)使用该版本的机器数量+预计出货量,请如实填写" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + size="middle" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + alignItems: 'baseline', + justifyContent: 'space-between', + width: '600px', + display: 'block', + }} + labelAlign="left" + colon={false} + > + <Button + style={{ margin: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickFirstBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickFirstBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ + boxShadow: 'rgba(31, 56, 88, 0.2) 0px 0px 0px 0px', + float: 'right', + }} + __events={{ + eventDataList: [], + eventList: [{ name: 'onClick', disabled: false }], + }} + > + 下一步 + </Button> + </Form.Item> + </Form> + </NextP> + )} + {!!__$$eval(() => this.state.currentStep === 1) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinish={function () { + this.onFinishSecond.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + height: '800px', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishSecond', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: true }, + ], + }} + initialValues={__$$eval( + () => this.state.customerProjectInfo + )} + onValuesChange={function () { + this.onValuesChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + <Form.Item + label="设备类型选择" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="projectModal" + requiredobj={{ + required: true, + message: '请选择设备类型', + }} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.projectModalsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择设备类型" + /> + </Form.Item> + <Form.Item + label="屏幕分辨率宽" + style={{ width: '600px' }} + name="displayWidth" + colon={false} + required={true} + requiredobj={{ + required: true, + message: '请输入屏幕分辨率宽', + }} + labelAlign="left" + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="例如1280" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕分辨率高" + style={{ width: '600px' }} + labelAlign="left" + colon={false} + name="displayHeight" + required={true} + requiredobj={{ + required: true, + message: '请输入屏幕分辨率高', + }} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="例如720" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕尺寸(inch)" + style={{ width: '600px' }} + name="displayInch" + labelAlign="left" + required={true} + colon={false} + requiredobj={{ + required: true, + message: '请输入屏幕尺寸', + }} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="请输入尺寸" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="屏幕DPI" + style={{ width: '600px' }} + labelAlign="left" + colon={false} + required={false} + name="displayDpi" + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="UI定制项目必填" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + min={0} + /> + </Form.Item> + <Form.Item + label="芯片名称" + colon={false} + required={true} + style={{ display: 'flex' }} + labelAlign="left" + extra="" + name="mainSoc" + requiredobj={{ + required: true, + message: '请输入芯片名称', + }} + lenobj={{ max: 50, message: '芯片名称不能超过50个字符' }} + > + <Input + placeholder="请输入芯片名称" + style={{ width: '600px' }} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="芯片核数" + style={{ width: '600px' }} + required={true} + requiredobj={{ + required: true, + message: '请输入芯片核数', + }} + name="cpuCoreNum" + labelAlign="left" + colon={false} + > + <InputNumber + value={3} + style={{ width: '600px' }} + placeholder="请输入芯片核数" + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + defaultValue="" + min={0} + /> + </Form.Item> + <Form.Item + label="指令集" + style={{ width: '600px' }} + required={true} + requiredobj={{ required: true, message: '请选择指令集' }} + name="instructions" + colon={false} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.instructionsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + /> + </Form.Item> + <Form.Item + label="系统版本" + labelAlign="left" + colon={false} + required={true} + style={{ width: '600px' }} + name="osVersion" + requiredobj={{ + required: true, + message: '请选择系统版本', + }} + > + <Select + style={{ width: '600px' }} + options={__$$eval(() => this.state.osVersionsArray)} + disabled={__$$eval( + () => + this.state.customerProjectInfo.id > 0 && + !this.state.isModifyStatus + )} + placeholder="请选择系统版本" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + width: '600px', + display: 'flex', + }} + > + <Button + style={{ marginLeft: '0' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickSecondBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickSecondBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ float: 'right', marginLeft: '20px' }} + loading={__$$eval( + () => + this.state.LOADING_ADD_OR_UPDATE_CUSTOMER_PROJECT + )} + > + {__$$eval(() => this.state.secondCommitText)} + </Button> + <Button + type="primary" + htmlType="submit" + style={{ marginLeft: '0px', float: 'right' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreSecond', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickPreSecond.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 上一步 + </Button> + </Form.Item> + </Form> + </NextP> + )} + {!!__$$eval(() => this.state.currentStep === 2) && ( + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + style={{ display: 'flex', justifyContent: 'center' }} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 10 }} + onFinishFailed={function () { + this.onFinishFailed.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + style={{ + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }} + layout="vertical" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinishFailed', + relatedEventName: 'onFinishFailed', + }, + ], + eventList: [ + { name: 'onFinish', disabled: false }, + { name: 'onFinishFailed', disabled: true }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label=""> + <Steps + current={1} + style={{ + width: '600px', + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + height: '300px', + }} + labelPlacement="horizontal" + direction="vertical" + > + <Steps.Step + title="提交完成" + description="" + style={{ width: '200px' }} + /> + <Steps.Step + title={__$$eval(() => this.state.thirdAuditText)} + subTitle="" + description="" + style={{ width: '200px' }} + /> + </Steps> + </Form.Item> + <Form.Item + wrapperCol={{ offset: '' }} + style={{ + flexDirection: 'row', + width: '600px', + display: 'flex', + }} + > + <Button + style={{ marginLeft: '0' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickThirdBack', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickThirdBack.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 返回 + </Button> + <Button + type="primary" + htmlType="submit" + style={{ float: 'right', marginLeft: '20px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickModifyThird', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickModifyThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + {__$$eval(() => this.state.thirdButtonText)} + </Button> + {!!__$$eval( + () => this.state.customerProjectInfo.status > 2 + ) && ( + <Button + type="primary" + htmlType="submit" + style={{ marginLeft: '0px', float: 'right' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreThird', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onClickPreThird.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 上一步 + </Button> + )} + </Form.Item> + </Form> + </NextP> + )} + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..6832d13682 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo7-literal-condition2/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/schema.json5 new file mode 100644 index 0000000000..4c829cfab1 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo7-literal-condition2/schema.json5 @@ -0,0 +1,1703 @@ +{ + version: '1.0.0', + componentsMap: [ + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Text', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextText', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Modal', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Modal', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Steps', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Step', + componentName: 'Steps.Step', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Steps', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Steps', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'P', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextP', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Input', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Input', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Item', + componentName: 'Form.Item', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Checkbox', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Group', + componentName: 'Checkbox.Group', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Select', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Select', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'DatePicker', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'DatePicker', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'InputNumber', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'InputNumber', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Button', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Button', + }, + { + package: '@alilc/antd-lowcode', + version: '0.8.0', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Form', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: 'Cell', + componentName: 'NextBlockCell', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextBlock', + }, + { + devMode: 'lowcode', + componentName: 'Slot', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Page', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextPage', + }, + { + devMode: 'lowcode', + componentName: 'Page', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_dockcviv8fo1', + props: { + ref: 'outterView', + style: { + height: '100%', + }, + }, + fileName: 'test', + dataSource: { + list: [], + }, + css: 'body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}', + lifeCycles: { + constructor: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + componentDidMount: { + type: 'JSFunction', + value: 'function() {}', + }, + componentDidUpdate: { + type: 'JSFunction', + value: 'function(prevProps, prevState, snapshot) {}', + }, + componentWillUnmount: { + type: 'JSFunction', + value: 'function() {}', + }, + }, + methods: { + __jp__init: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + __jp__initRouter: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + __jp__initDataSource: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + __jp__initEnv: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + __jp__initUtils: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + onFinishFirst: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + onClickPreSecond: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + onFinishSecond: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + onClickModifyThird: { + type: 'JSFunction', + value: "function() { /*...*/ }", + }, + onOkModifyDialogThird: { + type: 'JSFunction', + value: 'function() {\n //第三步 修改 对话框 确定\n\n this.setState({\n currentStep: 0,\n isModifyDialogVisible: false,\n });\n }', + }, + onCancelModifyDialogThird: { + type: 'JSFunction', + value: 'function() {\n //第三步 修改 对话框 取消\n\n this.setState({\n isModifyDialogVisible: false,\n });\n }', + }, + onFinishFailed: { + type: 'JSFunction', + value: 'function() {}', + }, + onClickPreThird: { + type: 'JSFunction', + value: 'function() {\n // 第三步 上一步\n this.setState({\n currentStep: 1,\n });\n }', + }, + onClickFirstBack: { + type: 'JSFunction', + value: "function() {\n // 第一步 返回按钮\n this.$router.push('/myProjectList');\n }", + }, + onClickSecondBack: { + type: 'JSFunction', + value: "function() {\n // 第二步 返回按钮\n this.$router.push('/myProjectList');\n }", + }, + onClickThirdBack: { + type: 'JSFunction', + value: "function() {\n // 第三步 返回按钮\n this.$router.push('/myProjectList');\n }", + }, + onValuesChange: { + type: 'JSFunction', + value: 'function(_, values) {\n this.setState({\n customerProjectInfo: {\n ...this.state.customerProjectInfo,\n ...values,\n },\n });\n }', + }, + }, + state: { + books: [], + currentStep: 0, + isModifyDialogVisible: false, + isModifyStatus: false, + secondCommitText: '完成并提交', + thirdAuditText: '审核中', + thirdButtonText: '修改', + customerProjectInfo: { + id: null, + systemProjectName: null, + projectVersionTypeArray: null, + projectVersionType: null, + versionLine: 2, + expectedTime: null, + expectedNum: null, + projectModal: null, + displayWidth: null, + displayHeight: null, + displayInch: null, + displayDpi: null, + mainSoc: null, + cpuCoreNum: null, + instructions: null, + osVersion: null, + status: null, + }, + versionLinesArray: [ + { + label: 'AmapAuto 485', + value: 1, + }, + { + label: 'AmapAuto 505', + value: 2, + }, + ], + projectModalsArray: [ + { + label: '车机', + value: 1, + }, + { + label: '车镜', + value: 2, + }, + { + label: '记录仪', + value: 3, + }, + { + label: '其他', + value: 4, + }, + ], + osVersionsArray: [ + { + label: '安卓5', + value: 1, + }, + { + label: '安卓6', + value: 2, + }, + { + label: '安卓7', + value: 3, + }, + { + label: '安卓8', + value: 4, + }, + { + label: '安卓9', + value: 5, + }, + { + label: '安卓10', + value: 6, + }, + ], + instructionsArray: [ + { + label: 'ARM64-V8', + value: 'ARM64-V8', + }, + { + label: 'ARM32-V7', + value: 'ARM32-V7', + }, + { + label: 'X86', + value: 'X86', + }, + { + label: 'X64', + value: 'X64', + }, + ], + }, + children: [ + { + componentName: 'Modal', + id: 'node_ockodngwu940', + props: { + title: '是否修改', + visible: { + type: 'JSExpression', + value: 'this.state.isModifyDialogVisible', + }, + okText: '确认', + okType: '', + forceRender: false, + cancelText: '取消', + zIndex: 2000, + destroyOnClose: false, + confirmLoading: false, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onOk', + relatedEventName: 'onOkModifyDialogThird', + }, + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onCancelModifyDialogThird', + }, + ], + eventList: [ + { + name: 'onCancel', + disabled: true, + }, + { + name: 'onOk', + disabled: true, + }, + ], + }, + onOk: { + type: 'JSFunction', + value: 'function(){this.onOkModifyDialogThird.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + onCancel: { + type: 'JSFunction', + value: 'function(){this.onCancelModifyDialogThird.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + hidden: true, + children: [ + { + componentName: 'NextText', + id: 'node_ockodngwu946', + props: { + type: 'inherit', + children: '修改将撤回此前填写的信息', + style: { + fontStyle: 'normal', + textAlign: 'left', + display: 'block', + fontFamily: 'arial, helvetica, microsoft yahei', + fontWeight: 'normal', + }, + }, + }, + ], + }, + { + componentName: 'NextPage', + id: 'node_ocko19zplh1', + props: { + columns: 12, + headerDivider: true, + placeholderStyle: { + gridRowEnd: 'span 1', + gridColumnEnd: 'span 12', + }, + placeholder: '页面主体内容:拖拽Block布局组件到这里', + header: { + type: 'JSSlot', + title: 'header', + }, + headerProps: { + background: 'surface', + }, + footer: { + type: 'JSSlot', + title: 'footer', + }, + minHeight: '100vh', + style: {}, + }, + title: '页面', + children: [ + { + componentName: 'NextBlock', + id: 'node_ocko19zplh2', + props: { + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + noPadding: false, + noBorder: false, + background: 'surface', + layoutmode: 'O', + colSpan: 12, + rowSpan: 1, + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ocko19zplh3', + props: { + title: '', + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + layoutmode: 'O', + childTotalColumns: 12, + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + children: [ + { + componentName: 'NextP', + id: 'node_ockoco6icv1w', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + style: { + marginBottom: '24px', + }, + }, + title: '段落', + children: [ + { + componentName: 'Steps', + id: 'node_ockoco6icv1x', + props: { + current: { + type: 'JSExpression', + value: 'this.state.currentStep', + }, + }, + children: [ + { + componentName: 'Steps.Step', + id: 'node_ockoco6icv1y', + props: { + title: '版本申请', + description: '', + }, + }, + { + componentName: 'Steps.Step', + id: 'node_ockoco6icv1z', + props: { + title: '机器配置', + subTitle: '', + description: '', + }, + }, + { + componentName: 'Steps.Step', + id: 'node_ockoco6icv20', + props: { + title: '项目审批', + description: '', + }, + }, + ], + }, + ], + }, + { + componentName: 'NextP', + id: 'node_ockoco6icv12w', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + full: true, + flex: true, + style: { + display: 'flex', + justifyContent: 'center', + }, + }, + title: '段落', + condition: { + type: 'JSExpression', + value: 'this.state.currentStep === 0', + }, + children: [ + { + componentName: 'Form', + id: 'node_ockoco6icv12x', + props: { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 10, + }, + onFinish: { + type: 'JSFunction', + value: 'function(){this.onFinishFirst.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + style: { + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }, + layout: 'vertical', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishFirst', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: true, + }, + { + name: 'onFinishFailed', + disabled: false, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: true, + }, + ], + }, + initialValues: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo', + }, + onValuesChange: { + type: 'JSFunction', + value: 'function(){this.onValuesChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ockojhvrkn2u', + props: { + label: '', + style: { + width: '600px', + }, + colon: false, + name: 'id', + }, + condition: false, + children: [ + { + componentName: 'Input', + id: 'node_ockojhvrkn2v', + props: { + placeholder: '', + style: { + width: '600px', + }, + bordered: false, + disabled: true, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv12y', + props: { + label: '版本类型选择', + name: 'projectVersionTypeArray', + initialValue: '', + labelAlign: 'left', + colon: false, + required: true, + style: { + flexDirection: 'column', + width: '600px', + }, + requiredobj: { + required: true, + message: '请选择版本类型', + }, + }, + children: [ + { + componentName: 'Checkbox.Group', + id: 'node_ockoco6icv12z', + props: { + options: [ + { + label: '基础版本', + value: '3', + }, + { + label: 'AR导航', + value: '1', + }, + { + label: '货车导航', + value: '2', + }, + { + label: 'UI定制', + value: '4', + disabled: false, + }, + ], + style: { + width: '600px', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv13a', + props: { + label: '版本线选择', + labelAlign: 'left', + colon: false, + required: true, + style: { + width: '600px', + }, + name: 'versionLine', + requiredobj: { + required: true, + message: '请选择版本线', + }, + extra: '', + }, + children: [ + { + componentName: 'Select', + id: 'node_ockoco6icv13b', + props: { + style: { + width: '600px', + }, + options: { + type: 'JSExpression', + value: 'this.state.versionLinesArray', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + placeholder: '请选择版本线', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv13o', + props: { + label: '项目名称', + colon: false, + required: true, + style: { + display: 'flex', + }, + labelAlign: 'left', + extra: '', + name: 'systemProjectName', + requiredobj: { + required: true, + message: '请按格式填写项目名称', + }, + typeobj: { + type: 'string', + message: '请输入项目名称,格式:公司简称-产品名称-版本类型', + }, + lenobj: { + max: 100, + message: '项目名称不能超过100个字符', + }, + }, + children: [ + { + componentName: 'Input', + id: 'node_ockoco6icv13p', + props: { + placeholder: '公司简称-产品名称-版本类型', + style: { + width: '600px', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv13v', + props: { + label: '预期交付时间', + style: { + width: '600px', + }, + colon: false, + required: true, + name: 'expectedTime', + labelAlign: 'left', + requiredobj: { + required: true, + message: '请填写预期交付时间', + }, + }, + children: [ + { + componentName: 'DatePicker', + id: 'node_ockoco6icv13w', + props: { + style: { + width: '600px', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bv8', + props: { + label: '预期出货量', + style: { + width: '600px', + }, + required: true, + requiredobj: { + required: true, + message: '请填写预期出货量', + }, + name: 'expectedNum', + labelAlign: 'left', + colon: false, + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bv9', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: '单位(台)使用该版本的机器数量+预计出货量,请如实填写', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + min: 0, + size: 'middle', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv130', + props: { + wrapperCol: { + offset: '', + }, + style: { + flexDirection: 'row', + alignItems: 'baseline', + justifyContent: 'space-between', + width: '600px', + display: 'block', + }, + labelAlign: 'left', + colon: false, + }, + children: [ + { + componentName: 'Button', + id: 'node_ockoco6icv132', + props: { + style: { + margin: '0px', + }, + children: '返回', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickFirstBack', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickFirstBack.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + { + componentName: 'Button', + id: 'node_ockoco6icv131', + props: { + type: 'primary', + children: '下一步', + htmlType: 'submit', + style: { + boxShadow: 'rgba(31, 56, 88, 0.2) 0px 0px 0px 0px', + float: 'right', + }, + __events: { + eventDataList: [], + eventList: [ + { + name: 'onClick', + disabled: false, + }, + ], + }, + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextP', + id: 'node_ockoco6icv1ue', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + full: true, + flex: true, + style: { + display: 'flex', + justifyContent: 'center', + }, + }, + title: '段落', + condition: { + type: 'JSExpression', + value: 'this.state.currentStep === 1', + }, + children: [ + { + componentName: 'Form', + id: 'node_ockoco6icv1uf', + props: { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 10, + }, + onFinish: { + type: 'JSFunction', + value: 'function(){this.onFinishSecond.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + style: { + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + height: '800px', + }, + layout: 'vertical', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinishSecond', + }, + { + type: 'componentEvent', + name: 'onValuesChange', + relatedEventName: 'onValuesChange', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: true, + }, + { + name: 'onFinishFailed', + disabled: false, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: true, + }, + ], + }, + initialValues: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo', + }, + onValuesChange: { + type: 'JSFunction', + value: 'function(){this.onValuesChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ockoco6icv1ui', + props: { + label: '设备类型选择', + labelAlign: 'left', + colon: false, + required: true, + style: { + width: '600px', + }, + name: 'projectModal', + requiredobj: { + required: true, + message: '请选择设备类型', + }, + }, + children: [ + { + componentName: 'Select', + id: 'node_ockoco6icv1uj', + props: { + style: { + width: '600px', + }, + options: { + type: 'JSExpression', + value: 'this.state.projectModalsArray', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + placeholder: '请选择设备类型', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bv17', + props: { + label: '屏幕分辨率宽', + style: { + width: '600px', + }, + name: 'displayWidth', + colon: false, + required: true, + requiredobj: { + required: true, + message: '请输入屏幕分辨率宽', + }, + labelAlign: 'left', + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bv18', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: '例如1280', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + min: 0, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bv10', + props: { + label: '屏幕分辨率高', + style: { + width: '600px', + }, + labelAlign: 'left', + colon: false, + name: 'displayHeight', + required: true, + requiredobj: { + required: true, + message: '请输入屏幕分辨率高', + }, + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bv11', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: '例如720', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + min: 0, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bvt', + props: { + label: '屏幕尺寸(inch)', + style: { + width: '600px', + }, + name: 'displayInch', + labelAlign: 'left', + required: true, + colon: false, + requiredobj: { + required: true, + message: '请输入屏幕尺寸', + }, + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bvu', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: '请输入尺寸', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + min: 0, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bvm', + props: { + label: '屏幕DPI', + style: { + width: '600px', + }, + labelAlign: 'left', + colon: false, + required: false, + name: 'displayDpi', + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bvn', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: 'UI定制项目必填', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + min: 0, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv1v3', + props: { + label: '芯片名称', + colon: false, + required: true, + style: { + display: 'flex', + }, + labelAlign: 'left', + extra: '', + name: 'mainSoc', + requiredobj: { + required: true, + message: '请输入芯片名称', + }, + lenobj: { + max: 50, + message: '芯片名称不能超过50个字符', + }, + }, + children: [ + { + componentName: 'Input', + id: 'node_ockoco6icv1v4', + props: { + placeholder: '请输入芯片名称', + style: { + width: '600px', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpmbs0bvf', + props: { + label: '芯片核数', + style: { + width: '600px', + }, + required: true, + requiredobj: { + required: true, + message: '请输入芯片核数', + }, + name: 'cpuCoreNum', + labelAlign: 'left', + colon: false, + }, + children: [ + { + componentName: 'InputNumber', + id: 'node_ockpmbs0bvg', + props: { + value: 3, + style: { + width: '600px', + }, + placeholder: '请输入芯片核数', + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + defaultValue: '', + min: 0, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockpxba11aa', + props: { + label: '指令集', + style: { + width: '600px', + }, + required: true, + requiredobj: { + required: true, + message: '请选择指令集', + }, + name: 'instructions', + colon: false, + }, + children: [ + { + componentName: 'Select', + id: 'node_ockpxba11ab', + props: { + style: { + width: '600px', + }, + options: { + type: 'JSExpression', + value: 'this.state.instructionsArray', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockodz1kiqh', + props: { + label: '系统版本', + labelAlign: 'left', + colon: false, + required: true, + style: { + width: '600px', + }, + name: 'osVersion', + requiredobj: { + required: true, + message: '请选择系统版本', + }, + }, + children: [ + { + componentName: 'Select', + id: 'node_ockodz1kiqi', + props: { + style: { + width: '600px', + }, + options: { + type: 'JSExpression', + value: 'this.state.osVersionsArray', + }, + disabled: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.id > 0 && ! this.state.isModifyStatus', + }, + placeholder: '请选择系统版本', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockoco6icv1uq', + props: { + wrapperCol: { + offset: '', + }, + style: { + flexDirection: 'row', + width: '600px', + display: 'flex', + }, + }, + children: [ + { + componentName: 'Button', + id: 'node_ockoco6icv1ur', + props: { + style: { + marginLeft: '0', + }, + children: '返回', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickSecondBack', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickSecondBack.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + { + componentName: 'Button', + id: 'node_ockoco6icv1vb', + props: { + type: 'primary', + children: { + type: 'JSExpression', + value: 'this.state.secondCommitText', + }, + htmlType: 'submit', + style: { + float: 'right', + marginLeft: '20px', + }, + loading: { + type: 'JSExpression', + value: 'this.state.LOADING_ADD_OR_UPDATE_CUSTOMER_PROJECT', + }, + }, + }, + { + componentName: 'Button', + id: 'node_ockoco6icv1us', + props: { + type: 'primary', + children: '上一步', + htmlType: 'submit', + style: { + marginLeft: '0px', + float: 'right', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreSecond', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickPreSecond.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextP', + id: 'node_ockodngwu9m', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + full: true, + flex: true, + style: { + display: 'flex', + justifyContent: 'center', + }, + }, + title: '段落', + condition: { + type: 'JSExpression', + value: 'this.state.currentStep === 2', + }, + children: [ + { + componentName: 'Form', + id: 'node_ockodngwu9n', + props: { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 10, + }, + onFinishFailed: { + type: 'JSFunction', + value: 'function(){this.onFinishFailed.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + style: { + display: 'flex', + flexDirection: 'column', + width: '600px', + justifyContent: 'center', + }, + layout: 'vertical', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinishFailed', + relatedEventName: 'onFinishFailed', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: false, + }, + { + name: 'onFinishFailed', + disabled: true, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: false, + }, + ], + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ockodngwu91m', + props: { + label: '', + }, + children: [ + { + componentName: 'Steps', + id: 'node_ockodngwu91n', + props: { + current: 1, + style: { + width: '600px', + display: 'flex', + justifyContent: 'space-around', + alignItems: 'center', + height: '300px', + }, + labelPlacement: 'horizontal', + direction: 'vertical', + }, + children: [ + { + componentName: 'Steps.Step', + id: 'node_ockodngwu91o', + props: { + title: '提交完成', + description: '', + style: { + width: '200px', + }, + }, + }, + { + componentName: 'Steps.Step', + id: 'node_ockodngwu91p', + props: { + title: { + type: 'JSExpression', + value: 'this.state.thirdAuditText', + }, + subTitle: '', + description: '', + style: { + width: '200px', + }, + }, + }, + ], + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ockodngwu914', + props: { + wrapperCol: { + offset: '', + }, + style: { + flexDirection: 'row', + width: '600px', + display: 'flex', + }, + }, + children: [ + { + componentName: 'Button', + id: 'node_ockodngwu915', + props: { + style: { + marginLeft: '0', + }, + children: '返回', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickThirdBack', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickThirdBack.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + { + componentName: 'Button', + id: 'node_ockodngwu916', + props: { + type: 'primary', + children: { + type: 'JSExpression', + value: 'this.state.thirdButtonText', + }, + htmlType: 'submit', + style: { + float: 'right', + marginLeft: '20px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickModifyThird', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickModifyThird.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + { + componentName: 'Button', + id: 'node_ockosjrkvr1d', + props: { + type: 'primary', + children: '上一步', + htmlType: 'submit', + style: { + marginLeft: '0px', + float: 'right', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onClickPreThird', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onClickPreThird.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + condition: { + type: 'JSExpression', + value: 'this.state.customerProjectInfo.status > 2', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + i18n: {}, +} diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json new file mode 100644 index 0000000000..91da69eef2 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/package.json @@ -0,0 +1,49 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-http-handler": "^1.0.0", + "@alilc/lowcode-components": "^1.0.0" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx new file mode 100644 index 0000000000..9a661ad753 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/pages/Example/index.jsx @@ -0,0 +1,116 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Page, Table } from '@alilc/lowcode-components'; + +import { createHttpHandler as __$$createHttpRequestHandler } from '@alilc/lowcode-datasource-http-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class Example$$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { http: __$$createHttpRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'userList', + type: 'http', + description: '用户列表', + options: function () { + return { + uri: 'https://api.example.com/user/list', + }; + }.bind(_this), + isInit: function () { + return undefined; + }.bind(_this), + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + <Table + dataSource={__$$eval(() => this.dataSourceMap['userList'])} + columns={[ + { dataIndex: 'name', title: '姓名' }, + { dataIndex: 'age', title: '年龄' }, + ]} + /> + </div> + ); + } +} + +export default Example$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..27e35dd605 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Example from '@/pages/Example'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Example, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo8-datasource-prop/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/schema.json5 new file mode 100644 index 0000000000..1e61996cf5 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo8-datasource-prop/schema.json5 @@ -0,0 +1,65 @@ +{ + version: '1.0.0', + componentsMap: [ + { + package: '@alilc/lowcode-components', + version: '^1.0.0', + componentName: 'Page', + destructuring: true, + exportName: 'Page', + }, + { + package: '@alilc/lowcode-components', + version: '^1.0.0', + componentName: 'Table', + destructuring: true, + exportName: 'Table', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_ockp6ci0hm1', + props: {}, + fileName: 'example', + dataSource: { + list: [ + { + id: 'userList', + type: 'http', + description: '用户列表', + options: { + uri: 'https://api.example.com/user/list', + }, + }, + ], + }, + children: [ + { + componentName: 'Table', + id: 'node_ockp6ci0hm22', + props: { + dataSource: { + type: 'DataSource', + id: 'userList', + }, + columns: [ + { + dataIndex: 'name', + title: '姓名', + }, + { + dataIndex: 'age', + title: '年龄', + }, + ], + }, + }, + ], + }, + ], + meta: { + name: 'example', + description: 'Example', + }, +} diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json new file mode 100644 index 0000000000..939adb7791 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/package.json @@ -0,0 +1,49 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "@alilc/lowcode-datasource-jsonp-handler": "^1.0.0", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx new file mode 100644 index 0000000000..799ca0d28f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/pages/$/index.jsx @@ -0,0 +1,125 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { Switch } from '@alifd/next'; + +import { createJsonpHandler as __$$createJsonpRequestHandler } from '@alilc/lowcode-datasource-jsonp-handler'; + +import { create as __$$createDataSourceEngine } from '@alilc/lowcode-datasource-engine/runtime'; + +import '@alifd/next/lib/switch/style'; + +import utils from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +class $$Page extends React.Component { + _context = this; + + _dataSourceConfig = this._defineDataSourceConfig(); + _dataSourceEngine = __$$createDataSourceEngine(this._dataSourceConfig, this, { + runtimeConfig: true, + requestHandlersMap: { jsonp: __$$createJsonpRequestHandler() }, + }); + + get dataSourceMap() { + return this._dataSourceEngine.dataSourceMap || {}; + } + + reloadDataSource = async () => { + await this._dataSourceEngine.reloadDataSource(); + }; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + __$$i18n._inject2(this); + + this.state = {}; + } + + $ = () => null; + + $$ = () => []; + + _defineDataSourceConfig() { + const _this = this; + return { + list: [ + { + id: 'todos', + isInit: function () { + return true; + }.bind(_this), + type: 'jsonp', + options: function () { + return { + method: 'GET', + uri: 'https://a0ee9135-6a7f-4c0f-a215-f0f247ad907d.mock.pstmn.io', + }; + }.bind(_this), + dataHandler: function dataHandler(data) { + return data.data; + }, + }, + ], + }; + } + + componentDidMount() { + this._dataSourceEngine.reloadDataSource(); + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div> + {__$$evalArray(() => this.dataSourceMap.todos.data).map((item, index) => + ((__$$context) => ( + <div> + <Switch + checkedChildren="开" + unCheckedChildren="关" + checked={__$$eval(() => item.done)} + /> + </div> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </div> + ); + } +} + +export default $$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..218549c42f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import $ from '@/pages/$'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: $, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo9-datasource-engine/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/schema.json5 new file mode 100644 index 0000000000..f91f132ad2 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo9-datasource-engine/schema.json5 @@ -0,0 +1,59 @@ +{ + version: '1.0.0', + componentsMap: [ + { + componentName: 'Switch', + package: '@alifd/next', + version: '1.19.18', + exportName: 'Switch', + destructuring: true, + subName: '', + }, + ], + componentsTree: [ + { + componentName: 'Page', + props: {}, + children: [ + { + componentName: 'Div', + props: {}, + children: [ + { + componentName: 'Switch', + props: { + checkedChildren: '开', + unCheckedChildren: '关', + checked: { + type: 'JSExpression', + value: 'this.item.done', + }, + }, + }, + ], + loop: { + type: 'JSExpression', + value: 'this.dataSourceMap.todos.data', + }, + }, + ], + dataSource: { + list: [ + { + id: 'todos', + isInit: true, + type: 'jsonp', + options: { + method: 'GET', + uri: 'https://a0ee9135-6a7f-4c0f-a215-f0f247ad907d.mock.pstmn.io', + }, + dataHandler: { + type: 'JSFunction', + value: 'function dataHandler(data) {return data.data;}', + }, + }, + ], + }, + }, + ], +} diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json new file mode 100644 index 0000000000..41f379d7a0 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/package.json @@ -0,0 +1,51 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode-materials": "0.9.4", + "@alife/mc-assets-1935": "0.1.42", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..066114aeeb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.css @@ -0,0 +1,8 @@ +body { + font-size: 12px; +} + +.botton { + width: 100px; + color: #ff00ff; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..922ad47ad8 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,822 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Button, + Typography, + Form, + Select, + Input, + ConfigProvider, + Tooltip, + Empty, +} from '@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js'; + +import { + AliAutoDiv, + AliAutoSearchTable, +} from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const AliAutoDivDefault = AliAutoDiv.default; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + }; + + this.__jp__init(); + this.statusDesc = { + 0: '失败', + 1: '成功', + 2: '构建中', + 3: '构建超时', + }; + this.pageParams = {}; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + if (window.arsenal) { + this.$router = new window.jianpin.ArsenalRouter({ + app: this.props.microApp, + }); + } else { + this.$router = new window.jianpin.ArsenalRouter(); + } + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initConfig() { + /*...*/ + } + + __jp__initUtils() { + this.$utils = { + message: window.jianpin.utils.message, + axios: window.jianpin.utils.axios, + moment: window.jianpin.utils.moment, + }; + } + + fetchPkgs() { + /*...*/ + } + + onPageChange(pageIndex, pageSize) { + this.pageParams = { + pageIndex, + pageSize, + }; + this.fetchPkgs(); + } + + renderTime(time) { + return this.$utils.moment(time).format('YYYY-MM-DD HH:mm'); + } + + renderUserName(user) { + return user.user_name; + } + + reload() { + /*...*/ + } + + handleResult() { + /*...*/ + } + + handleDetail() { + // 跳转详情页面 TODO + } + + onResultCancel() { + this.setState({ + resultVisible: false, + }); + } + + formatResult(item) { + if (!item) { + return '暂无结果'; + } + const { channel, plat, version, status } = item; + return [channel, plat, version, status].join('-'); + } + + handleDownload() { + /*...*/ + } + + onFinish() { + /*...*/ + } + + componentDidMount() { + this.$ds.resolve('PROJECTS', { + params: { + size: 5000, + }, + }); + // if (this.state.init === false) { + // this.setState({ + // init: true, + // }); + // } + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="查看结果" + visible={__$$eval(() => this.state.resultVisible)} + footer={ + <Button + type="primary" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 确定 + </Button> + } + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + width="720px" + centered={true} + > + {__$$evalArray(() => this.state.results).map((item, index) => + ((__$$context) => ( + <AliAutoDivDefault style={{ width: '100%' }}> + {!!__$$eval( + () => + __$$context.state.results && + __$$context.state.results.length > 0 + ) && ( + <AliAutoDivDefault + style={{ + width: '100%', + textAlign: 'left', + marginBottom: '10px', + }} + > + <Button + type="primary" + size="small" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.handleDownload.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(__$$context)} + > + 下载全部 + </Button> + </AliAutoDivDefault> + )} + <Typography.Text> + {__$$eval(() => __$$context.formatResult(item))} + </Typography.Text> + {!!__$$eval(() => item.download_link) && ( + <Typography.Link + href={__$$eval(() => item.download_link)} + target="_blank" + > + {' '} + - 点击下载 + </Typography.Link> + )} + {!!__$$eval(() => item.release_notes) && ( + <Typography.Link + href={__$$eval(() => item.release_notes)} + target="_blank" + > + {' '} + - 跳转发布节点 + </Typography.Link> + )} + </AliAutoDivDefault> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface' }} + footer={null} + minHeight="100vh" + > + <NextBlock + prefix="next-" + placeholderStyle={{ height: '100%' }} + noPadding={false} + noBorder={false} + background="surface" + layoutmode="O" + colSpan={12} + rowSpan={1} + childTotalColumns={12} + > + <NextBlockCell + title="" + prefix="next-" + placeholderStyle={{ height: '100%' }} + layoutmode="O" + childTotalColumns={12} + isAutoContainer={true} + colSpan={12} + rowSpan={1} + > + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + full={true} + flex={true} + > + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onFinish.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + layout="inline" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + > + <Form.Item label="项目名称/渠道号" name="channel_id"> + <Select + style={{ width: '280px' }} + options={__$$eval(() => this.state.projects)} + showArrow={true} + tokenSeparators={[]} + showSearch={true} + /> + </Form.Item> + <Form.Item label="版本号" name="buildId"> + <Input + placeholder="请输入" + style={{ width: '280px' }} + size="middle" + /> + </Form.Item> + <Form.Item label="构建人" name="user_id"> + <Select + style={{ width: 200 }} + options={[ + { label: 'A', value: 'A' }, + { label: 'B', value: 'B' }, + { label: 'C', value: 'C' }, + ]} + showSearch={true} + /> + </Form.Item> + <Form.Item label="ID" name="id"> + <Input placeholder="请输入" style={{ width: '160px' }} /> + </Form.Item> + <Form.Item wrapperCol={{ offset: 6 }}> + <Button type="primary" htmlType="submit"> + 查询 + </Button> + </Form.Item> + </Form> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <ConfigProvider locale="zh-CN"> + {!!__$$eval( + () => + !this.state.isSearch || + (this.state.isSearch && this.state.pkgs.length > 0) + ) && ( + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.pkgs)} + columns={[ + { + title: 'ID', + dataIndex: 'id', + key: 'name', + width: 80, + }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: (text, record, index) => + ((__$$context) => + __$$evalArray(() => text.split(',')).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ display: 'block' }} + > + {__$$eval(() => item)} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: (text, record, index) => + ((__$$context) => ( + <Tooltip + title={__$$evalArray(() => text || []).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ + display: 'block', + color: '#FFFFFF', + }} + > + {__$$eval( + () => + item.channelId + + ' / ' + + item.version + )} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + )} + > + <Typography.Text> + {__$$eval(() => text[0].version)} + </Typography.Text> + </Tooltip> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { title: '构建Job', dataIndex: 'job_name', width: 180 }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: (text, record, index) => + ((__$$context) => [ + <Typography.Text> + {__$$eval(() => __$$context.statusDesc[text])} + </Typography.Text>, + !!__$$eval(() => text === 2) && ( + <Icon + type="SyncOutlined" + size={16} + spin={true} + style={{ marginLeft: '10px' }} + /> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: function () { + return this.renderTime.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: function () { + return this.renderUserName.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href={__$$eval(() => text)} + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href="http://rivermap.alibaba.net/dashboard/testExecute" + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { title: '触发源', dataIndex: 'source', width: 120 }, + { + title: '详情', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleDetail.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleResult.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + ghost={false} + href={__$$eval(() => text)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: (text, record, index) => + ((__$$context) => ( + <Button + type="text" + children="" + icon={ + <Icon + type="ReloadOutlined" + size={14} + color="#0593d3" + style={{ + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }} + spin={false} + /> + } + shape="circle" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.reload.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + /> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + fixed: 'right', + }, + ]} + actions={[]} + pagination={{ + total: __$$eval(() => this.state.total), + defaultPageSize: 8, + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + }} + scrollX={1200} + /> + )} + </ConfigProvider> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => this.state.pkgs.length < 1 && this.state.isSearch + ) && <Empty description="暂无数据" />} + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..6832d13682 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_10-jsslot/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/schema.json5 new file mode 100644 index 0000000000..a499dfc650 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_10-jsslot/schema.json5 @@ -0,0 +1,1206 @@ +{ + version: '1.0.0', + componentsMap: [ + { + devMode: 'lowcode', + componentName: 'Slot', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Button', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Button', + }, + { + package: '@alife/mc-assets-1935', + version: '0.1.42', + exportName: 'AliAutoDiv', + main: 'build/lowcode/index.js', + destructuring: true, + subName: 'default', + componentName: 'AliAutoDivDefault', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Typography', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Text', + componentName: 'Typography.Text', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Typography', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Link', + componentName: 'Typography.Link', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Modal', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Modal', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Select', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Select', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Item', + componentName: 'Form.Item', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Input', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Input', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Form', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'P', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextP', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: 'Cell', + componentName: 'NextBlockCell', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextBlock', + }, + { + package: '@alife/mc-assets-1935', + version: '0.1.42', + exportName: 'AliAutoSearchTable', + main: 'build/lowcode/index.js', + destructuring: true, + subName: 'default', + componentName: 'AliAutoSearchTableDefault', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'ConfigProvider', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'ConfigProvider', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Empty', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Empty', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Page', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextPage', + }, + { + devMode: 'lowcode', + componentName: 'Page', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.9.4', + exportName: 'Tooltip', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Tooltip', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_dockcviv8fo1', + props: { + ref: 'outterView', + style: { + height: '100%', + }, + }, + fileName: 'test', + dataSource: { + list: [], + }, + css: 'body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}', + lifeCycles: { + constructor: { + type: 'JSFunction', + value: "function() {\n this.__jp__init();\n this.statusDesc = {\n 0: '失败',\n 1: '成功',\n 2: '构建中',\n 3: '构建超时',\n };\n this.pageParams = {};\n }", + }, + componentDidMount: { + type: 'JSFunction', + value: "function() {\n this.$ds.resolve('PROJECTS', {\n params: {\n size: 5000,\n },\n });\n // if (this.state.init === false) {\n // this.setState({\n // init: true,\n // });\n // }\n }", + }, + componentDidUpdate: { + type: 'JSFunction', + value: 'function(prevProps, prevState, snapshot) {}', + }, + componentWillUnmount: { + type: 'JSFunction', + value: 'function() {}', + }, + }, + methods: { + __jp__init: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initRouter: { + type: 'JSFunction', + value: 'function() {\n if (window.arsenal) {\n this.$router = new window.jianpin.ArsenalRouter({\n app: this.props.microApp,\n });\n } else {\n this.$router = new window.jianpin.ArsenalRouter();\n }\n}', + }, + __jp__initDataSource: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initEnv: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initConfig: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initUtils: { + type: 'JSFunction', + value: 'function() {\n this.$utils = {\n message: window.jianpin.utils.message,\n axios: window.jianpin.utils.axios,\n moment: window.jianpin.utils.moment,\n };\n}', + }, + fetchPkgs: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + onPageChange: { + type: 'JSFunction', + value: 'function(pageIndex, pageSize) {\n this.pageParams = {\n pageIndex,\n pageSize,\n };\n this.fetchPkgs();\n }', + }, + renderTime: { + type: 'JSFunction', + value: "function(time) {\n return this.$utils.moment(time).format('YYYY-MM-DD HH:mm');\n }", + }, + renderUserName: { + type: 'JSFunction', + value: 'function(user) {\n return user.user_name;\n }', + }, + reload: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleResult: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleDetail: { + type: 'JSFunction', + value: 'function() {\n // 跳转详情页面 TODO\n }', + }, + onResultCancel: { + type: 'JSFunction', + value: 'function() {\n this.setState({\n resultVisible: false,\n });\n }', + }, + formatResult: { + type: 'JSFunction', + value: "function(item) {\n if (!item) {\n return '暂无结果';\n }\n const { channel, plat, version, status } = item;\n return [channel, plat, version, status].join('-');\n }", + }, + handleDownload: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + onFinish: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + }, + state: { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + }, + children: [ + { + componentName: 'Modal', + id: 'node_ocksh9yppxb', + props: { + title: '查看结果', + visible: { + type: 'JSExpression', + value: 'this.state.resultVisible', + }, + footer: { + type: 'JSSlot', + value: [ + { + componentName: 'Button', + id: 'node_ocksh9yppxf', + props: { + type: 'primary', + children: '确定', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onResultCancel.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { + name: 'onCancel', + disabled: true, + }, + { + name: 'onOk', + disabled: false, + }, + ], + }, + onCancel: { + type: 'JSFunction', + value: 'function(){this.onResultCancel.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: '720px', + centered: true, + }, + hidden: true, + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockshazuxa4', + props: { + style: { + width: '100%', + }, + }, + loop: { + type: 'JSExpression', + value: 'this.state.results', + }, + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockshazuxai', + props: { + style: { + width: '100%', + textAlign: 'left', + marginBottom: '10px', + }, + }, + condition: { + type: 'JSExpression', + value: 'this.state.results && this.state.results.length > 0', + }, + children: [ + { + componentName: 'Button', + id: 'node_ockshazuxah', + props: { + type: 'primary', + children: '下载全部', + size: 'small', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleDownload.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + { + componentName: 'Typography.Text', + id: 'node_ockshazuxa5', + props: { + children: { + type: 'JSExpression', + value: 'this.formatResult(this.item)', + }, + }, + }, + { + componentName: 'Typography.Link', + id: 'node_ockshazuxa6', + props: { + href: { + type: 'JSExpression', + value: 'this.item.download_link', + }, + target: '_blank', + children: ' - 点击下载', + }, + condition: { + type: 'JSExpression', + value: 'this.item.download_link', + }, + }, + { + componentName: 'Typography.Link', + id: 'node_ockshazuxa7', + props: { + href: { + type: 'JSExpression', + value: 'this.item.release_notes', + }, + target: '_blank', + children: ' - 跳转发布节点', + }, + condition: { + type: 'JSExpression', + value: 'this.item.release_notes', + }, + }, + ], + }, + ], + }, + { + componentName: 'NextPage', + id: 'node_ocko19zplh1', + props: { + columns: 12, + headerDivider: true, + placeholderStyle: { + gridRowEnd: 'span 1', + gridColumnEnd: 'span 12', + }, + placeholder: '页面主体内容:拖拽Block布局组件到这里', + header: { + type: 'JSSlot', + title: 'header', + }, + headerProps: { + background: 'surface', + }, + footer: { + type: 'JSSlot', + title: 'footer', + }, + minHeight: '100vh', + }, + title: '页面', + children: [ + { + componentName: 'NextBlock', + id: 'node_ocko19zplh2', + props: { + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + noPadding: false, + noBorder: false, + background: 'surface', + layoutmode: 'O', + colSpan: 12, + rowSpan: 1, + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ocko19zplh3', + props: { + title: '', + prefix: 'next-', + placeholderStyle: { + height: '100%', + }, + layoutmode: 'O', + childTotalColumns: 12, + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + children: [ + { + componentName: 'NextP', + id: 'node_ocks8dtt1ms', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + full: true, + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'Form', + id: 'node_ocks8dtt1mt', + props: { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 14, + }, + onFinish: { + type: 'JSFunction', + value: 'function(){this.onFinish.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + layout: 'inline', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: true, + }, + { + name: 'onFinishFailed', + disabled: false, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: false, + }, + ], + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1mz', + props: { + label: '项目名称/渠道号', + name: 'channel_id', + }, + children: [ + { + componentName: 'Select', + id: 'node_ocksfuhwhsd', + props: { + style: { + width: '280px', + }, + options: { + type: 'JSExpression', + value: 'this.state.projects', + }, + showArrow: true, + tokenSeparators: [], + showSearch: true, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m12', + props: { + label: '版本号', + name: 'buildId', + }, + children: [ + { + componentName: 'Input', + id: 'node_ocksfuhwhs3', + props: { + placeholder: '请输入', + style: { + width: '280px', + }, + size: 'middle', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m18', + props: { + label: '构建人', + name: 'user_id', + }, + children: [ + { + componentName: 'Select', + id: 'node_ocksfuhwhsi', + props: { + style: { + width: 200, + }, + options: [ + { + label: 'A', + value: 'A', + }, + { + label: 'B', + value: 'B', + }, + { + label: 'C', + value: 'C', + }, + ], + showSearch: true, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m19', + props: { + label: 'ID', + name: 'id', + }, + children: [ + { + componentName: 'Input', + id: 'node_ocksfuhwhs8', + props: { + placeholder: '请输入', + style: { + width: '160px', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1mw', + props: { + wrapperCol: { + offset: 6, + }, + }, + children: [ + { + componentName: 'Button', + id: 'node_ocks8dtt1mx', + props: { + type: 'primary', + children: '查询', + htmlType: 'submit', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextBlock', + id: 'node_ockshc4ifn1b', + props: { + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ockshc4ifn1c', + props: { + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + title: '子区块', + children: [ + { + componentName: 'NextP', + id: 'node_ockshc4ifn1d', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'ConfigProvider', + id: 'node_ockshc4ifn1e', + props: { + locale: 'zh-CN', + }, + children: [ + { + componentName: 'AliAutoSearchTableDefault', + id: 'node_ocksfuhwhsx', + props: { + rowKey: 'key', + dataSource: { + type: 'JSExpression', + value: 'this.state.pkgs', + }, + columns: [ + { + title: 'ID', + dataIndex: 'id', + key: 'name', + width: 80, + }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksh2bq0428', + props: { + children: { + type: 'JSExpression', + value: 'this.item', + }, + style: { + display: 'block', + }, + }, + loop: { + type: 'JSExpression', + value: "this.text.split(',')", + }, + }, + ], + }, + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Tooltip', + id: 'node_ocksso4xavj', + props: { + title: { + type: 'JSSlot', + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksso4xavn', + props: { + children: { + type: 'JSExpression', + value: "this.item. channelId + ' / ' + this.item.version", + }, + style: { + display: 'block', + color: '#FFFFFF', + }, + }, + loop: { + type: 'JSExpression', + value: 'this.text || []', + }, + }, + ], + }, + }, + children: [ + { + componentName: 'Typography.Text', + id: 'node_ocksso4xavm', + props: { + children: { + type: 'JSExpression', + value: 'this.text[0].version', + }, + }, + }, + ], + }, + ], + }, + width: 120, + }, + { + title: '构建Job', + dataIndex: 'job_name', + width: 180, + }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksh3jkxzw', + props: { + children: { + type: 'JSExpression', + value: 'this.statusDesc[this.text]', + }, + }, + }, + { + componentName: 'Icon', + id: 'node_ocksh3jkxzx', + props: { + type: 'SyncOutlined', + size: 16, + spin: true, + style: { + marginLeft: '10px', + }, + }, + condition: { + type: 'JSExpression', + value: 'this.text === 2', + }, + }, + ], + }, + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: { + type: 'JSFunction', + value: 'function(){ return this.renderTime.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: { + type: 'JSFunction', + value: 'function(){ return this.renderUserName.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Link', + id: 'node_ocksh64kbx21', + props: { + href: { + type: 'JSExpression', + value: 'this.text', + }, + target: '_blank', + children: '查看', + }, + condition: { + type: 'JSExpression', + value: 'this.text', + }, + }, + { + componentName: 'Typography.Text', + id: 'node_ocksh64kbx22', + props: { + children: '暂无', + }, + condition: { + type: 'JSExpression', + value: '!this.text', + }, + }, + ], + }, + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Link', + id: 'node_ocksh3jkxz3e', + props: { + href: 'http://rivermap.alibaba.net/dashboard/testExecute', + target: '_blank', + children: '查看', + }, + condition: { + type: 'JSExpression', + value: 'this.text', + }, + }, + { + componentName: 'Typography.Text', + id: 'node_ocksh3jkxz3f', + props: { + children: '暂无', + }, + condition: { + type: 'JSExpression', + value: '!this.text', + }, + }, + ], + }, + }, + { + title: '触发源', + dataIndex: 'source', + width: 120, + }, + { + title: '详情', + dataIndex: 'id', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh8yryw7', + props: { + type: 'link', + children: '查看', + size: 'small', + style: { + padding: '0px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleDetail.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh9v6jw7', + props: { + type: 'link', + children: '查看', + size: 'small', + style: { + padding: '0px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleResult.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + ghost: false, + href: { + type: 'JSExpression', + value: 'this.text', + }, + }, + }, + ], + }, + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh96rad1g', + props: { + type: 'text', + children: '', + icon: { + type: 'JSSlot', + value: [ + { + componentName: 'Icon', + id: 'node_ocksh96rad1j', + props: { + type: 'ReloadOutlined', + size: 14, + color: '#0593d3', + style: { + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }, + spin: false, + }, + }, + ], + }, + shape: 'circle', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.reload.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + fixed: 'right', + }, + ], + actions: [], + pagination: { + total: { + type: 'JSExpression', + value: 'this.state.total', + }, + defaultPageSize: 8, + onPageChange: { + type: 'JSFunction', + value: 'function(){ return this.onPageChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + scrollX: 1200, + }, + condition: { + type: 'JSExpression', + value: '!this.state.isSearch || (this.state.isSearch && this.state.pkgs.length > 0)', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextBlock', + id: 'node_ocksk6f8fa3b', + props: { + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ocksk6f8fa3c', + props: { + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + title: '子区块', + children: [ + { + componentName: 'NextP', + id: 'node_ocksk6f8fa3d', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'Empty', + id: 'node_ocksk6f8fa3e', + props: { + description: '暂无数据', + }, + condition: { + type: 'JSExpression', + value: 'this.state.pkgs.length < 1 && this.state.isSearch', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + i18n: {}, +} diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json new file mode 100644 index 0000000000..2b45dfc53f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/package.json @@ -0,0 +1,51 @@ +{ + "name": "icejs-demo-app", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "react-router": "^5.2.1", + "@alifd/theme-design-pro": "^0.x", + "intl-messageformat": "^9.3.6", + "@ice/store": "^1.4.3", + "@loadable/component": "^5.15.2", + "@alilc/lowcode-datasource-engine": "^1.0.0", + "undefined": "*", + "@alilc/antd-lowcode-materials": "0.11.0", + "@alife/mc-assets-1935": "0.1.43", + "@alife/container": "0.3.7" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/public/index.html diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js new file mode 100644 index 0000000000..266d8ef71d --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/app.js @@ -0,0 +1,11 @@ +import { createApp } from 'ice'; + +const appConfig = { + app: { + rootId: 'app', + }, + router: { + type: 'hash', + }, +}; +createApp(appConfig); diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/constants.js new file mode 100644 index 0000000000..ea766c9da3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/constants.js @@ -0,0 +1,3 @@ +const __$$constants = {}; + +export default __$$constants; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss new file mode 100644 index 0000000000..82ca3eac73 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/global.scss @@ -0,0 +1,6 @@ +// 引入默认全局样式 +@import '@alifd/next/reset.scss'; + +body { + -webkit-font-smoothing: antialiased; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js new file mode 100644 index 0000000000..1334d2502b --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/i18n.js @@ -0,0 +1,77 @@ +const i18nConfig = {}; + +let locale = + typeof navigator === 'object' && typeof navigator.language === 'string' + ? navigator.language + : 'zh-CN'; + +const getLocale = () => locale; + +const setLocale = (target) => { + locale = target; +}; + +const isEmptyVariables = (variables) => + (Array.isArray(variables) && variables.length === 0) || + (typeof variables === 'object' && + (!variables || Object.keys(variables).length === 0)); + +// 按低代码规范里面的要求进行变量替换 +const format = (msg, variables) => + typeof msg === 'string' + ? msg.replace(/\$?\{(\w+)\}/g, (match, key) => variables?.[key] ?? '') + : msg; + +const i18nFormat = ({ id, defaultMessage, fallback }, variables) => { + const msg = + i18nConfig[locale]?.[id] ?? + i18nConfig[locale.replace('-', '_')]?.[id] ?? + defaultMessage; + if (msg == null) { + console.warn('[i18n]: unknown message id: %o (locale=%o)', id, locale); + return fallback === undefined ? `${id}` : fallback; + } + + return format(msg, variables); +}; + +const i18n = (id, params) => { + return i18nFormat({ id }, params); +}; + +// 将国际化的一些方法注入到目标对象&上下文中 +const _inject2 = (target) => { + target.i18n = i18n; + target.getLocale = getLocale; + target.setLocale = (locale) => { + setLocale(locale); + target.forceUpdate(); + }; + target._i18nText = (t) => { + // 优先取直接传过来的语料 + const localMsg = t[locale] ?? t[String(locale).replace('-', '_')]; + if (localMsg != null) { + return format(localMsg, t.params); + } + + // 其次用项目级别的 + const projectMsg = i18nFormat({ id: t.key, fallback: null }, t.params); + if (projectMsg != null) { + return projectMsg; + } + + // 兜底用 use 指定的或默认语言的 + return format(t[t.use || 'zh-CN'] ?? t.en_US, t.params); + }; + + // 注入到上下文中去 + if (target._context && target._context !== target) { + Object.assign(target._context, { + i18n, + getLocale, + setLocale: target.setLocale, + }); + } +}; + +export { getLocale, setLocale, i18n, i18nFormat, _inject2 }; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..066114aeeb --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.css @@ -0,0 +1,8 @@ +body { + font-size: 12px; +} + +.botton { + width: 100px; + color: #ff00ff; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx new file mode 100644 index 0000000000..5630342f37 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/pages/Test/index.jsx @@ -0,0 +1,976 @@ +// 注意: 出码引擎注入的临时变量默认都以 "__$$" 开头,禁止在搭建的代码中直接访问。 +// 例外:react 框架的导出名和各种组件名除外。 +import React from 'react'; + +import { + Modal, + Button, + Typography, + Form, + Select, + Input, + Tooltip, + Icon, + Empty, +} from '@alilc/antd-lowcode-materials/dist/antd-lowcode.esm.js'; + +import { + AliAutoDiv, + AliAutoSearchTable, +} from '@alife/mc-assets-1935/build/lowcode/index.js'; + +import { + Page as NextPage, + Block as NextBlock, + P as NextP, +} from '@alife/container/lib/index.js'; + +import utils, { RefsManager } from '../../utils'; + +import * as __$$i18n from '../../i18n'; + +import __$$constants from '../../constants'; + +import './index.css'; + +const AliAutoDivDefault = AliAutoDiv.default; + +const AliAutoSearchTableDefault = AliAutoSearchTable.default; + +const NextBlockCell = NextBlock.Cell; + +class Test$$Page extends React.Component { + _context = this; + + get constants() { + return __$$constants || {}; + } + + constructor(props, context) { + super(props); + + this.utils = utils; + + this._refsManager = new RefsManager(); + + __$$i18n._inject2(this); + + this.state = { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + userOptions: [], + searchValues: { user_id: '', channel_id: '' }, + }; + + this.__jp__init(); + this.statusDesc = { + 0: '失败', + 1: '成功', + 2: '构建中', + 3: '构建超时', + }; + this.pageParams = {}; + this.searchParams = {}; + this.userTimeout = null; + this.currentUser = null; + this.notFoundContent = null; + this.projectTimeout = null; + this.currentProject = null; + } + + $ = (refName) => { + return this._refsManager.get(refName); + }; + + $$ = (refName) => { + return this._refsManager.getAll(refName); + }; + + componentDidUpdate(prevProps, prevState, snapshot) {} + + componentWillUnmount() {} + + __jp__init() { + /*...*/ + } + + __jp__initRouter() { + /*...*/ + } + + __jp__initDataSource() { + /*...*/ + } + + __jp__initEnv() { + /*...*/ + } + + __jp__initConfig() { + /*...*/ + } + + __jp__initUtils() { + /*...*/ + } + + setSearchItem() { + /*...*/ + } + + fetchProject() { + /*...*/ + } + + handleProjectSearch() { + /*...*/ + } + + handleProjectChange(id) { + this.setSearchItem({ + channel_id: id, + }); + } + + fetchUser() { + /*...*/ + } + + handleUserSearch() { + /*...*/ + } + + handleUserChange(user) { + console.log('debug user', user); + this.setSearchItem({ + user_id: user, + }); + } + + fetchPkgs() { + /*...*/ + } + + onPageChange(pageIndex, pageSize) { + this.pageParams = { + pageIndex, + pageSize, + }; + this.fetchPkgs(); + } + + renderTime(time) { + return this.$utils.moment(time).format('YYYY-MM-DD HH:mm'); + } + + renderUserName(user) { + return user.user_name; + } + + reload() { + /*...*/ + } + + handleResult() { + /*...*/ + } + + handleDetail() { + /*...*/ + } + + onResultCancel() { + /*...*/ + } + + formatResult() { + /*...*/ + } + + handleDownload() { + /*...*/ + } + + onFinish() { + /*...*/ + } + + componentDidMount() { + this.$ds.resolve('PROJECTS'); + if (this.userTimeout) { + clearTimeout(this.userTimeout); + this.userTimeout = null; + } + if (this.projectTimeout) { + clearTimeout(this.projectTimeout); + this.projectTimeout = null; + } + } + + render() { + const __$$context = this._context || this; + const { state } = __$$context; + return ( + <div + ref={this._refsManager.linkRef('outterView')} + style={{ height: '100%' }} + > + <Modal + title="查看结果" + visible={__$$eval(() => this.state.resultVisible)} + footer={ + <Button + type="primary" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 确定 + </Button> + } + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { name: 'onCancel', disabled: true }, + { name: 'onOk', disabled: false }, + ], + }} + onCancel={function () { + this.onResultCancel.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + width="720px" + centered={true} + closable={true} + keyboard={true} + mask={true} + maskClosable={true} + > + <AliAutoDivDefault style={{ width: '100%' }}> + {!!__$$eval( + () => this.state.results && this.state.results.length > 0 + ) && ( + <AliAutoDivDefault + style={{ + width: '100%', + textAlign: 'left', + marginBottom: '16px', + }} + > + <Button + type="primary" + size="small" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [{ name: 'onClick', disabled: true }], + }} + onClick={function () { + this.handleDownload.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + > + 下载全部 + </Button> + </AliAutoDivDefault> + )} + {__$$evalArray(() => this.state.results).map((item, index) => + ((__$$context) => ( + <AliAutoDivDefault style={{ width: '100%', marginTop: '10px' }}> + <Typography.Text> + {__$$eval(() => __$$context.formatResult(item))} + </Typography.Text> + {!!__$$eval(() => item.download_link) && ( + <Typography.Link + href={__$$eval(() => item.download_link)} + target="_blank" + > + {' '} + - 点击下载 + </Typography.Link> + )} + {!!__$$eval(() => item.release_notes) && ( + <Typography.Link + href={__$$eval(() => item.release_notes)} + target="_blank" + > + {' '} + - 跳转发布节点 + </Typography.Link> + )} + </AliAutoDivDefault> + ))(__$$createChildContext(__$$context, { item, index })) + )} + </AliAutoDivDefault> + </Modal> + <NextPage + columns={12} + headerDivider={true} + placeholderStyle={{ gridRowEnd: 'span 1', gridColumnEnd: 'span 12' }} + placeholder="页面主体内容:拖拽Block布局组件到这里" + header={null} + headerProps={{ background: 'surface', style: { padding: '' } }} + footer={null} + minHeight="100vh" + contentProps={{ noPadding: false, background: 'transparent' }} + > + <NextBlock childTotalColumns={12}> + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + <AliAutoDivDefault style={{ width: '100%', display: 'flex' }}> + <AliAutoDivDefault style={{ flex: '1' }}> + <Form + labelCol={{ span: 10 }} + wrapperCol={{ span: 14 }} + onFinish={function () { + this.onFinish.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + name="basic" + layout="inline" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { name: 'onFinish', disabled: true }, + { name: 'onFinishFailed', disabled: false }, + { name: 'onFieldsChange', disabled: false }, + { name: 'onValuesChange', disabled: false }, + ], + }} + colon={true} + labelAlign="right" + preserve={true} + scrollToFirstError={true} + size="middle" + values={__$$eval(() => this.state.searchValues)} + > + <Form.Item + label="项目名称/渠道号" + name="channel_id" + labelAlign="right" + colon={true} + > + <Select + style={{ width: '320px' }} + options={__$$eval(() => this.state.projects)} + showArrow={false} + tokenSeparators={[]} + showSearch={true} + defaultActiveFirstOption={true} + size="middle" + bordered={true} + filterOption={true} + optionFilterProp="label" + allowClear={true} + placeholder="请输入项目名称/渠道号" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleProjectChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleProjectSearch', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: true }, + { name: 'onSelect', disabled: false }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }} + onChange={function () { + this.handleProjectChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onSearch={function () { + this.handleProjectSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + /> + </Form.Item> + <Form.Item label="版本号" name="buildId"> + <Input + placeholder="请输入版本号" + style={{ width: '180px' }} + size="middle" + bordered={true} + /> + </Form.Item> + <Form.Item label="构建人" name="user_id"> + <Select + style={{ width: '210px' }} + options={__$$eval(() => this.state.userOptions)} + showSearch={true} + defaultActiveFirstOption={false} + size="middle" + bordered={true} + filterOption={true} + optionFilterProp="label" + notFoundContent={__$$eval( + () => this.userNotFoundContent + )} + showArrow={false} + placeholder="请输入构建人" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleUserChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleUserSearch', + }, + ], + eventList: [ + { name: 'onBlur', disabled: false }, + { name: 'onChange', disabled: true }, + { name: 'onDeselect', disabled: false }, + { name: 'onFocus', disabled: false }, + { name: 'onInputKeyDown', disabled: false }, + { name: 'onMouseEnter', disabled: false }, + { name: 'onMouseLeave', disabled: false }, + { name: 'onPopupScroll', disabled: false }, + { name: 'onSearch', disabled: true }, + { name: 'onSelect', disabled: false }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }} + onChange={function () { + this.handleUserChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + onSearch={function () { + this.handleUserSearch.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this)} + allowClear={true} + /> + </Form.Item> + <Form.Item + label="ID" + name="id" + labelAlign="right" + colon={true} + > + <Input + placeholder="请输入ID" + style={{ width: '180px' }} + bordered={true} + size="middle" + /> + </Form.Item> + <Form.Item + wrapperCol={{ offset: 6 }} + labelAlign="right" + colon={true} + style={{ flex: '1', textAlign: 'right' }} + > + <Button + type="primary" + htmlType="submit" + shape="default" + size="middle" + > + 查询 + </Button> + </Form.Item> + </Form> + </AliAutoDivDefault> + <AliAutoDivDefault style={{}}> + <Button + type="link" + htmlType="button" + shape="default" + size="middle" + > + 新增打包 + </Button> + </AliAutoDivDefault> + </AliAutoDivDefault> + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock + childTotalColumns={12} + mode="inset" + layoutmode="O" + autolayout="(12|1)" + > + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => + !this.state.isSearch || + (this.state.isSearch && this.state.pkgs.length > 0) + ) && ( + <AliAutoSearchTableDefault + rowKey="key" + dataSource={__$$eval(() => this.state.pkgs)} + columns={[ + { title: 'ID', dataIndex: 'id', key: 'name', width: 80 }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: (text, record, index) => + ((__$$context) => + __$$evalArray(() => text.split(',')).map( + (item, index) => + ((__$$context) => ( + <Typography.Text style={{ display: 'block' }}> + {__$$eval(() => item)} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: (text, record, index) => + ((__$$context) => ( + <Tooltip + title={__$$evalArray(() => text || []).map( + (item, index) => + ((__$$context) => ( + <Typography.Text + style={{ + display: 'block', + color: '#FFFFFF', + }} + > + {__$$eval( + () => + item.channelId + ' / ' + item.version + )} + </Typography.Text> + ))( + __$$createChildContext(__$$context, { + item, + index, + }) + ) + )} + > + <Typography.Text> + {__$$eval(() => text[0].version)} + </Typography.Text> + </Tooltip> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { title: '构建Job', dataIndex: 'job_name', width: 180 }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: (text, record, index) => + ((__$$context) => [ + <Typography.Text> + {__$$eval(() => __$$context.statusDesc[text])} + </Typography.Text>, + !!__$$eval(() => text === 2) && ( + <Icon + type="SyncOutlined" + size={16} + spin={true} + style={{ marginLeft: '10px' }} + /> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: function () { + return this.renderTime.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: function () { + return this.renderUserName.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href={__$$eval(() => text)} + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: (text, record, index) => + ((__$$context) => [ + !!__$$eval(() => text) && ( + <Typography.Link + href="http://rivermap.alibaba.net/dashboard/testExecute" + target="_blank" + > + 查看 + </Typography.Link> + ), + !!__$$eval(() => !text) && ( + <Typography.Text>暂无</Typography.Text> + ), + ])( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + }, + { title: '触发源', dataIndex: 'source', width: 120 }, + { + title: '详情', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleDetail.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: (text, record, index) => + ((__$$context) => ( + <Button + type="link" + size="small" + style={{ padding: '0px' }} + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.handleResult.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + ghost={false} + href={__$$eval(() => text)} + > + 查看 + </Button> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: (text, record, index) => + ((__$$context) => ( + <Button + type="text" + children="" + icon={ + <Icon + type="ReloadOutlined" + size={14} + color="#0593d3" + style={{ + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }} + spin={false} + /> + } + shape="circle" + __events={{ + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { name: 'onClick', disabled: true }, + ], + }} + onClick={function () { + this.reload.apply( + this, + Array.prototype.slice + .call(arguments) + .concat([]) + ); + }.bind(__$$context)} + /> + ))( + __$$createChildContext(__$$context, { + text, + record, + index, + }) + ), + fixed: 'right', + }, + ]} + actions={[]} + pagination={{ + total: __$$eval(() => this.state.total), + defaultPageSize: 10, + onPageChange: function () { + return this.onPageChange.apply( + this, + Array.prototype.slice.call(arguments).concat([]) + ); + }.bind(this), + defaultPageIndex: 1, + }} + scrollX={1200} + isPagination={true} + /> + )} + </NextP> + </NextBlockCell> + </NextBlock> + <NextBlock + childTotalColumns={12} + mode="inset" + layoutmode="O" + autolayout="(12|1)" + > + <NextBlockCell isAutoContainer={true} colSpan={12} rowSpan={1}> + <NextP + wrap={false} + type="body2" + verAlign="middle" + textSpacing={true} + align="left" + flex={true} + > + {!!__$$eval( + () => this.state.pkgs.length < 1 && this.state.isSearch + ) && <Empty description="暂无数据" />} + </NextP> + </NextBlockCell> + </NextBlock> + </NextPage> + </div> + ); + } +} + +export default Test$$Page; + +function __$$eval(expr) { + try { + return expr(); + } catch (error) {} +} + +function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; +} + +function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; +} diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js new file mode 100644 index 0000000000..6832d13682 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/routes.js @@ -0,0 +1,18 @@ +import Test from '@/pages/Test'; + +import BasicLayout from '@/layouts/BasicLayout'; + +const routerConfig = [ + { + path: '/', + component: BasicLayout, + children: [ + { + path: '', + component: Test, + }, + ], + }, +]; + +export default routerConfig; diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js new file mode 100644 index 0000000000..1190717924 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/src/utils.js @@ -0,0 +1,47 @@ +import { createRef } from 'react'; + +export class RefsManager { + constructor() { + this.refInsStore = {}; + } + + clearNullRefs() { + Object.keys(this.refInsStore).forEach((refName) => { + const filteredInsList = this.refInsStore[refName].filter( + (insRef) => !!insRef.current + ); + if (filteredInsList.length > 0) { + this.refInsStore[refName] = filteredInsList; + } else { + delete this.refInsStore[refName]; + } + }); + } + + get(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName][0].current; + } + + return null; + } + + getAll(refName) { + this.clearNullRefs(); + if (this.refInsStore[refName] && this.refInsStore[refName].length > 0) { + return this.refInsStore[refName].map((i) => i.current); + } + + return []; + } + + linkRef(refName) { + const refIns = createRef(); + this.refInsStore[refName] = this.refInsStore[refName] || []; + this.refInsStore[refName].push(refIns); + return refIns; + } +} + +export default {}; diff --git a/modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/schema.json5 new file mode 100644 index 0000000000..2bd00adda3 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-app/demo_11-jsslot-2/schema.json5 @@ -0,0 +1,1457 @@ +{ + version: '1.0.0', + componentsMap: [ + { + devMode: 'lowcode', + componentName: 'Slot', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Button', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Button', + }, + { + package: '@alife/mc-assets-1935', + version: '0.1.43', + exportName: 'AliAutoDiv', + main: 'build/lowcode/index.js', + destructuring: true, + subName: 'default', + componentName: 'AliAutoDivDefault', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Typography', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Text', + componentName: 'Typography.Text', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Typography', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Link', + componentName: 'Typography.Link', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Modal', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Modal', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Select', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Select', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + subName: 'Item', + componentName: 'Form.Item', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Input', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Input', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Form', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Form', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'P', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextP', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: 'Cell', + componentName: 'NextBlockCell', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Block', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextBlock', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Tooltip', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Tooltip', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Icon', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Icon', + }, + { + package: '@alife/mc-assets-1935', + version: '0.1.43', + exportName: 'AliAutoSearchTable', + main: 'build/lowcode/index.js', + destructuring: true, + subName: 'default', + componentName: 'AliAutoSearchTableDefault', + }, + { + package: '@alilc/antd-lowcode-materials', + version: '0.11.0', + exportName: 'Empty', + main: 'dist/antd-lowcode.esm.js', + destructuring: true, + componentName: 'Empty', + }, + { + package: '@alife/container', + version: '0.3.7', + exportName: 'Page', + main: 'lib/index.js', + destructuring: true, + subName: '', + componentName: 'NextPage', + }, + { + devMode: 'lowcode', + componentName: 'Page', + }, + ], + componentsTree: [ + { + componentName: 'Page', + id: 'node_dockcviv8fo1', + props: { + ref: 'outterView', + style: { + height: '100%', + }, + }, + fileName: 'test', + dataSource: { + list: [], + }, + css: 'body {\n font-size: 12px;\n}\n\n.botton {\n width: 100px;\n color: #ff00ff\n}', + lifeCycles: { + constructor: { + type: 'JSFunction', + value: "function() {\n this.__jp__init();\n this.statusDesc = {\n 0: '失败',\n 1: '成功',\n 2: '构建中',\n 3: '构建超时',\n };\n this.pageParams = {};\n this.searchParams = {};\n this.userTimeout = null;\n this.currentUser = null;\n this.notFoundContent = null;\n this.projectTimeout = null;\n this.currentProject = null;\n }", + }, + componentDidMount: { + type: 'JSFunction', + value: "function() {\n this.$ds.resolve('PROJECTS');\n if (this.userTimeout) {\n clearTimeout(this.userTimeout);\n this.userTimeout = null;\n }\n if (this.projectTimeout) {\n clearTimeout(this.projectTimeout);\n this.projectTimeout = null;\n }\n }", + }, + componentDidUpdate: { + type: 'JSFunction', + value: 'function(prevProps, prevState, snapshot) {}', + }, + componentWillUnmount: { + type: 'JSFunction', + value: 'function() {}', + }, + }, + methods: { + __jp__init: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initRouter: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initDataSource: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initEnv: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initConfig: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + __jp__initUtils: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + setSearchItem: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + fetchProject: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleProjectSearch: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleProjectChange: { + type: 'JSFunction', + value: 'function(id) {\n this.setSearchItem({\n channel_id: id,\n });\n }', + }, + fetchUser: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleUserSearch: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleUserChange: { + type: 'JSFunction', + value: "function(user) {\n console.log('debug user', user);\n this.setSearchItem({\n user_id: user,\n });\n }", + }, + fetchPkgs: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + onPageChange: { + type: 'JSFunction', + value: 'function(pageIndex, pageSize) {\n this.pageParams = {\n pageIndex,\n pageSize,\n };\n this.fetchPkgs();\n }', + }, + renderTime: { + type: 'JSFunction', + value: "function(time) {\n return this.$utils.moment(time).format('YYYY-MM-DD HH:mm');\n }", + }, + renderUserName: { + type: 'JSFunction', + value: 'function(user) {\n return user.user_name;\n }', + }, + reload: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleResult: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleDetail: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + onResultCancel: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + formatResult: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + handleDownload: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + onFinish: { + type: 'JSFunction', + value: 'function() { /*...*/ }', + }, + }, + state: { + pkgs: [], + total: 0, + isSearch: false, + projects: [], + results: [], + resultVisible: false, + userOptions: [], + searchValues: { + user_id: '', + channel_id: '', + }, + }, + children: [ + { + componentName: 'Modal', + id: 'node_ocksh9yppxb', + props: { + title: '查看结果', + visible: { + type: 'JSExpression', + value: 'this.state.resultVisible', + }, + footer: { + type: 'JSSlot', + value: [ + { + componentName: 'Button', + id: 'node_ocksh9yppxf', + props: { + type: 'primary', + children: '确定', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.onResultCancel.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onCancel', + relatedEventName: 'onResultCancel', + }, + ], + eventList: [ + { + name: 'onCancel', + disabled: true, + }, + { + name: 'onOk', + disabled: false, + }, + ], + }, + onCancel: { + type: 'JSFunction', + value: 'function(){this.onResultCancel.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: '720px', + centered: true, + closable: true, + keyboard: true, + mask: true, + maskClosable: true, + }, + hidden: true, + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockshazuxa4', + props: { + style: { + width: '100%', + }, + }, + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockshazuxai', + props: { + style: { + width: '100%', + textAlign: 'left', + marginBottom: '16px', + }, + }, + condition: { + type: 'JSExpression', + value: 'this.state.results && this.state.results.length > 0', + }, + children: [ + { + componentName: 'Button', + id: 'node_ockshazuxah', + props: { + type: 'primary', + children: '下载全部', + size: 'small', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDownload', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleDownload.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + { + componentName: 'AliAutoDivDefault', + id: 'node_ockt2muyfi4', + props: { + style: { + width: '100%', + marginTop: '10px', + }, + }, + loop: { + type: 'JSExpression', + value: 'this.state.results', + }, + children: [ + { + componentName: 'Typography.Text', + id: 'node_ockshazuxa5', + props: { + children: { + type: 'JSExpression', + value: 'this.formatResult(this.item)', + }, + }, + }, + { + componentName: 'Typography.Link', + id: 'node_ockshazuxa6', + props: { + href: { + type: 'JSExpression', + value: 'this.item.download_link', + }, + target: '_blank', + children: ' - 点击下载', + }, + condition: { + type: 'JSExpression', + value: 'this.item.download_link', + }, + }, + { + componentName: 'Typography.Link', + id: 'node_ockshazuxa7', + props: { + href: { + type: 'JSExpression', + value: 'this.item.release_notes', + }, + target: '_blank', + children: ' - 跳转发布节点', + }, + condition: { + type: 'JSExpression', + value: 'this.item.release_notes', + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextPage', + id: 'node_ocko19zplh1', + props: { + columns: 12, + headerDivider: true, + placeholderStyle: { + gridRowEnd: 'span 1', + gridColumnEnd: 'span 12', + }, + placeholder: '页面主体内容:拖拽Block布局组件到这里', + header: { + type: 'JSSlot', + title: 'header', + }, + headerProps: { + background: 'surface', + style: { + padding: '', + }, + }, + footer: { + type: 'JSSlot', + title: 'footer', + }, + minHeight: '100vh', + contentProps: { + noPadding: false, + background: 'transparent', + }, + }, + title: '页面', + children: [ + { + componentName: 'NextBlock', + id: 'node_ockt3t4q8565', + props: { + childTotalColumns: 12, + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ockt3t4q8566', + props: { + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + title: '子区块', + children: [ + { + componentName: 'NextP', + id: 'node_ockt3t4q8567', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockt3t4q8568', + props: { + style: { + width: '100%', + display: 'flex', + }, + }, + children: [ + { + componentName: 'AliAutoDivDefault', + id: 'node_ockt3t4q857a', + props: { + style: { + flex: '1', + }, + }, + children: [ + { + componentName: 'Form', + id: 'node_ocks8dtt1mt', + props: { + labelCol: { + span: 10, + }, + wrapperCol: { + span: 14, + }, + onFinish: { + type: 'JSFunction', + value: 'function(){this.onFinish.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + name: 'basic', + layout: 'inline', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onFinish', + relatedEventName: 'onFinish', + }, + ], + eventList: [ + { + name: 'onFinish', + disabled: true, + }, + { + name: 'onFinishFailed', + disabled: false, + }, + { + name: 'onFieldsChange', + disabled: false, + }, + { + name: 'onValuesChange', + disabled: false, + }, + ], + }, + colon: true, + labelAlign: 'right', + preserve: true, + scrollToFirstError: true, + size: 'middle', + values: { + type: 'JSExpression', + value: 'this.state.searchValues', + }, + }, + children: [ + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1mz', + props: { + label: '项目名称/渠道号', + name: 'channel_id', + labelAlign: 'right', + colon: true, + }, + children: [ + { + componentName: 'Select', + id: 'node_ocksfuhwhsd', + props: { + style: { + width: '320px', + }, + options: { + type: 'JSExpression', + value: 'this.state.projects', + }, + showArrow: false, + tokenSeparators: [], + showSearch: true, + defaultActiveFirstOption: true, + size: 'middle', + bordered: true, + filterOption: true, + optionFilterProp: 'label', + allowClear: true, + placeholder: '请输入项目名称/渠道号', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleProjectChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleProjectSearch', + }, + ], + eventList: [ + { + name: 'onBlur', + disabled: false, + }, + { + name: 'onChange', + disabled: true, + }, + { + name: 'onDeselect', + disabled: false, + }, + { + name: 'onFocus', + disabled: false, + }, + { + name: 'onInputKeyDown', + disabled: false, + }, + { + name: 'onMouseEnter', + disabled: false, + }, + { + name: 'onMouseLeave', + disabled: false, + }, + { + name: 'onPopupScroll', + disabled: false, + }, + { + name: 'onSearch', + disabled: true, + }, + { + name: 'onSelect', + disabled: false, + }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }, + onChange: { + type: 'JSFunction', + value: 'function(){this.handleProjectChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + onSearch: { + type: 'JSFunction', + value: 'function(){this.handleProjectSearch.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m12', + props: { + label: '版本号', + name: 'buildId', + }, + children: [ + { + componentName: 'Input', + id: 'node_ocksfuhwhs3', + props: { + placeholder: '请输入版本号', + style: { + width: '180px', + }, + size: 'middle', + bordered: true, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m18', + props: { + label: '构建人', + name: 'user_id', + }, + children: [ + { + componentName: 'Select', + id: 'node_ocksfuhwhsi', + props: { + style: { + width: '210px', + }, + options: { + type: 'JSExpression', + value: 'this.state.userOptions', + }, + showSearch: true, + defaultActiveFirstOption: false, + size: 'middle', + bordered: true, + filterOption: true, + optionFilterProp: 'label', + notFoundContent: { + type: 'JSExpression', + value: 'this.userNotFoundContent', + }, + showArrow: false, + placeholder: '请输入构建人', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onChange', + relatedEventName: 'handleUserChange', + }, + { + type: 'componentEvent', + name: 'onSearch', + relatedEventName: 'handleUserSearch', + }, + ], + eventList: [ + { + name: 'onBlur', + disabled: false, + }, + { + name: 'onChange', + disabled: true, + }, + { + name: 'onDeselect', + disabled: false, + }, + { + name: 'onFocus', + disabled: false, + }, + { + name: 'onInputKeyDown', + disabled: false, + }, + { + name: 'onMouseEnter', + disabled: false, + }, + { + name: 'onMouseLeave', + disabled: false, + }, + { + name: 'onPopupScroll', + disabled: false, + }, + { + name: 'onSearch', + disabled: true, + }, + { + name: 'onSelect', + disabled: false, + }, + { + name: 'onDropdownVisibleChange', + disabled: false, + }, + ], + }, + onChange: { + type: 'JSFunction', + value: 'function(){this.handleUserChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + onSearch: { + type: 'JSFunction', + value: 'function(){this.handleUserSearch.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + allowClear: true, + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1m19', + props: { + label: 'ID', + name: 'id', + labelAlign: 'right', + colon: true, + }, + children: [ + { + componentName: 'Input', + id: 'node_ocksfuhwhs8', + props: { + placeholder: '请输入ID', + style: { + width: '180px', + }, + bordered: true, + size: 'middle', + }, + }, + ], + }, + { + componentName: 'Form.Item', + id: 'node_ocks8dtt1mw', + props: { + wrapperCol: { + offset: 6, + }, + labelAlign: 'right', + colon: true, + style: { + flex: '1', + textAlign: 'right', + }, + }, + children: [ + { + componentName: 'Button', + id: 'node_ocks8dtt1mx', + props: { + type: 'primary', + children: '查询', + htmlType: 'submit', + shape: 'default', + size: 'middle', + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'AliAutoDivDefault', + id: 'node_ockt3t4q856b', + props: { + style: {}, + }, + children: [ + { + componentName: 'Button', + id: 'node_ockt3t4q85y', + props: { + type: 'link', + children: '新增打包', + htmlType: 'button', + shape: 'default', + size: 'middle', + }, + condition: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextBlock', + id: 'node_ockshc4ifn1b', + props: { + childTotalColumns: 12, + mode: 'inset', + layoutmode: 'O', + autolayout: '(12|1)', + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ockshc4ifn1c', + props: { + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + title: '子区块', + children: [ + { + componentName: 'NextP', + id: 'node_ockshc4ifn1d', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'AliAutoSearchTableDefault', + id: 'node_ocksfuhwhsx', + props: { + rowKey: 'key', + dataSource: { + type: 'JSExpression', + value: 'this.state.pkgs', + }, + columns: [ + { + title: 'ID', + dataIndex: 'id', + key: 'name', + width: 80, + }, + { + title: '渠道号', + dataIndex: 'channels', + key: 'age', + width: 142, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksh2bq0428', + props: { + children: { + type: 'JSExpression', + value: 'this.item', + }, + style: { + display: 'block', + }, + }, + loop: { + type: 'JSExpression', + value: "this.text.split(',')", + }, + }, + ], + }, + }, + { + title: '版本号', + dataIndex: 'dic_version', + key: 'address', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Tooltip', + id: 'node_ocksts0jqgj', + props: { + title: { + type: 'JSSlot', + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksts0jqgn', + props: { + children: { + type: 'JSExpression', + value: "this.item. channelId + ' / ' + this.item.version", + }, + style: { + display: 'block', + color: '#FFFFFF', + }, + }, + loop: { + type: 'JSExpression', + value: 'this.text || []', + }, + }, + ], + }, + }, + children: [ + { + componentName: 'Typography.Text', + id: 'node_ocksts0jqgm', + props: { + children: { + type: 'JSExpression', + value: 'this.text[0].version', + }, + }, + }, + ], + }, + ], + }, + width: 120, + }, + { + title: '构建Job', + dataIndex: 'job_name', + width: 180, + }, + { + title: '构建类型', + dataIndex: 'packaging_type', + width: 94, + }, + { + title: '构建状态', + dataIndex: 'status', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Text', + id: 'node_ocksh3jkxzw', + props: { + children: { + type: 'JSExpression', + value: 'this.statusDesc[this.text]', + }, + }, + }, + { + componentName: 'Icon', + id: 'node_ocksh3jkxzx', + props: { + type: 'SyncOutlined', + size: 16, + spin: true, + style: { + marginLeft: '10px', + }, + }, + condition: { + type: 'JSExpression', + value: 'this.text === 2', + }, + }, + ], + }, + width: 100, + }, + { + title: '构建时间', + dataIndex: 'start_time', + render: { + type: 'JSFunction', + value: 'function(){ return this.renderTime.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: 148, + }, + { + title: '构建人', + dataIndex: 'user', + render: { + type: 'JSFunction', + value: 'function(){ return this.renderUserName.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + width: 80, + }, + { + title: 'Jenkins 链接', + dataIndex: 'jenkins_link', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Link', + id: 'node_ocksh64kbx21', + props: { + href: { + type: 'JSExpression', + value: 'this.text', + }, + target: '_blank', + children: '查看', + }, + condition: { + type: 'JSExpression', + value: 'this.text', + }, + }, + { + componentName: 'Typography.Text', + id: 'node_ocksh64kbx22', + props: { + children: '暂无', + }, + condition: { + type: 'JSExpression', + value: '!this.text', + }, + }, + ], + }, + width: 120, + }, + { + title: '测试平台链接', + dataIndex: 'is_run_testing', + width: 120, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Typography.Link', + id: 'node_ocksh3jkxz3e', + props: { + href: 'http://rivermap.alibaba.net/dashboard/testExecute', + target: '_blank', + children: '查看', + }, + condition: { + type: 'JSExpression', + value: 'this.text', + }, + }, + { + componentName: 'Typography.Text', + id: 'node_ocksh3jkxz3f', + props: { + children: '暂无', + }, + condition: { + type: 'JSExpression', + value: '!this.text', + }, + }, + ], + }, + }, + { + title: '触发源', + dataIndex: 'source', + width: 120, + }, + { + title: '详情', + dataIndex: 'id', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh8yryw7', + props: { + type: 'link', + children: '查看', + size: 'small', + style: { + padding: '0px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleDetail', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleDetail.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + width: 80, + fixed: 'right', + }, + { + title: '结果', + dataIndex: 'id', + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh9v6jw7', + props: { + type: 'link', + children: '查看', + size: 'small', + style: { + padding: '0px', + }, + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'handleResult', + paramStr: 'this.text', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.handleResult.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + ghost: false, + href: { + type: 'JSExpression', + value: 'this.text', + }, + }, + }, + ], + }, + width: 80, + fixed: 'right', + }, + { + title: '重新执行', + dataIndex: 'id', + width: 92, + render: { + type: 'JSSlot', + params: ['text', 'record', 'index'], + value: [ + { + componentName: 'Button', + id: 'node_ocksh96rad1g', + props: { + type: 'text', + children: '', + icon: { + type: 'JSSlot', + value: [ + { + componentName: 'Icon', + id: 'node_ocksh96rad1j', + props: { + type: 'ReloadOutlined', + size: 14, + color: '#0593d3', + style: { + padding: '3px', + border: '1px solid #0593d3', + borderRadius: '14px', + cursor: 'pointer', + height: '22px', + }, + spin: false, + }, + }, + ], + }, + shape: 'circle', + __events: { + eventDataList: [ + { + type: 'componentEvent', + name: 'onClick', + relatedEventName: 'reload', + }, + ], + eventList: [ + { + name: 'onClick', + disabled: true, + }, + ], + }, + onClick: { + type: 'JSFunction', + value: 'function(){this.reload.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + }, + }, + ], + }, + fixed: 'right', + }, + ], + actions: [], + pagination: { + total: { + type: 'JSExpression', + value: 'this.state.total', + }, + defaultPageSize: 10, + onPageChange: { + type: 'JSFunction', + value: 'function(){ return this.onPageChange.apply(this,Array.prototype.slice.call(arguments).concat([])) }', + }, + defaultPageIndex: 1, + }, + scrollX: 1200, + isPagination: true, + }, + condition: { + type: 'JSExpression', + value: '!this.state.isSearch || (this.state.isSearch && this.state.pkgs.length > 0)', + }, + }, + ], + }, + ], + }, + ], + }, + { + componentName: 'NextBlock', + id: 'node_ocksk6f8fa3b', + props: { + childTotalColumns: 12, + mode: 'inset', + layoutmode: 'O', + autolayout: '(12|1)', + }, + title: '区块', + children: [ + { + componentName: 'NextBlockCell', + id: 'node_ocksk6f8fa3c', + props: { + isAutoContainer: true, + colSpan: 12, + rowSpan: 1, + }, + title: '子区块', + children: [ + { + componentName: 'NextP', + id: 'node_ocksk6f8fa3d', + props: { + wrap: false, + type: 'body2', + verAlign: 'middle', + textSpacing: true, + align: 'left', + flex: true, + }, + title: '段落', + children: [ + { + componentName: 'Empty', + id: 'node_ocksk6f8fa3e', + props: { + description: '暂无数据', + }, + condition: { + type: 'JSExpression', + value: 'this.state.pkgs.length < 1 && this.state.isSearch', + }, + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + i18n: {}, +} diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.editorconfig b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.editorconfig similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.editorconfig rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.editorconfig diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.eslintignore b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.eslintignore similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.eslintignore rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.eslintignore diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.eslintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.eslintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.eslintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.eslintrc.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.gitignore b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.gitignore new file mode 100644 index 0000000000..4ec178818e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.gitignore @@ -0,0 +1,25 @@ + +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +node_modules/ + +# production +build/ +dist/ +tmp/ +lib/ + +# misc +.idea/ +.happypack +.DS_Store +*.swp +*.dia~ +.ice + +npm-debug.log* +yarn-debug.log* +yarn-error.log* +index.module.scss.d.ts + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.prettierignore b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.prettierignore similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.prettierignore rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.prettierignore diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.prettierrc.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.prettierrc.js similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.prettierrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.prettierrc.js diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.stylelintignore b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.stylelintignore similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.stylelintignore rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.stylelintignore diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.stylelintrc.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.stylelintrc.js similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/.stylelintrc.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/.stylelintrc.js diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/README.md b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/README.md similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/README.md rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/README.md diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/abc.json b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/abc.json similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/abc.json rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/abc.json diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/build.json b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/build.json similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/build.json rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/build.json diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/jsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/jsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/jsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/jsconfig.json diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/package.json b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/package.json new file mode 100644 index 0000000000..d32b684dcf --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/package.json @@ -0,0 +1,44 @@ +{ + "name": "@alifd/scaffold-lite-js", + "version": "0.1.5", + "description": "轻量级模板,使用 JavaScript,仅包含基础的 Layout。", + "dependencies": { + "moment": "^2.24.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "@alifd/theme-design-pro": "^0.x", + "@alifd/next": "1.19.18" + }, + "devDependencies": { + "@ice/spec": "^1.0.0", + "build-plugin-fusion": "^0.1.0", + "build-plugin-moment-locales": "^0.1.0", + "eslint": "^6.0.1", + "ice.js": "^1.0.0", + "stylelint": "^13.2.0", + "@ali/build-plugin-ice-def": "^0.1.0" + }, + "scripts": { + "start": "icejs start", + "build": "icejs build", + "lint": "npm run eslint && npm run stylelint", + "eslint": "eslint --cache --ext .js,.jsx ./", + "stylelint": "stylelint ./**/*.scss" + }, + "ideMode": { + "name": "ice-react" + }, + "iceworks": { + "type": "react", + "adapter": "adapter-react-v3" + }, + "engines": { + "node": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "http://gitlab.xxx.com/msd/leak-scan/tree/master" + }, + "private": true, + "originTemplate": "@alifd/scaffold-lite-js" +} diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/public/index.html b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/public/index.html similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/public/index.html rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/public/index.html diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/app.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/app.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/app.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/app.js diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/constants.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/constants.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/constants.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/constants.js diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/global.scss b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/global.scss similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/global.scss rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/global.scss diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/i18n.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/i18n.js similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/i18n.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/i18n.js diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx new file mode 100644 index 0000000000..cc70d53bea --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.jsx @@ -0,0 +1,14 @@ + +import React from 'react'; +import styles from './index.module.scss'; + +export default function Footer() { + return ( + <p className={styles.footer}> + <span className={styles.logo}>Alibaba Fusion</span> + <br /> + <span className={styles.copyright}>© 2019-现在 Alibaba Fusion & ICE</span> + </p> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss new file mode 100644 index 0000000000..81e77fda5f --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Footer/index.module.scss @@ -0,0 +1,15 @@ + +.footer { + line-height: 20px; + text-align: center; +} + +.logo { + font-weight: bold; + font-size: 16px; +} + +.copyright { + font-size: 12px; +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx new file mode 100644 index 0000000000..265bfdaa07 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.jsx @@ -0,0 +1,16 @@ + +import React from 'react'; +import { Link } from 'ice'; +import styles from './index.module.scss'; + +export default function Logo({ image, text, url }) { + return ( + <div className="logo"> + <Link to={url || '/'} className={styles.logo}> + {image && <img src={image} alt="logo" />} + <span>{text}</span> + </Link> + </div> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/Logo/index.module.scss diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/components/PageNav/index.jsx diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx new file mode 100644 index 0000000000..18db44df5e --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/index.jsx @@ -0,0 +1,81 @@ + +import React, { useState } from 'react'; +import { Shell, ConfigProvider } from '@alifd/next'; +import PageNav from './components/PageNav'; +import Logo from './components/Logo'; +import Footer from './components/Footer'; + +(function() { + const throttle = function(type, name, obj = window) { + let running = false; + + const func = () => { + if (running) { + return; + } + + running = true; + requestAnimationFrame(() => { + obj.dispatchEvent(new CustomEvent(name)); + running = false; + }); + }; + + obj.addEventListener(type, func); + }; + + throttle('resize', 'optimizedResize'); +})(); + +export default function BasicLayout({ children }) { + const getDevice = width => { + const isPhone = + typeof navigator !== 'undefined' && navigator && navigator.userAgent.match(/phone/gi); + + if (width < 680 || isPhone) { + return 'phone'; + } + if (width < 1280 && width > 680) { + return 'tablet'; + } + return 'desktop'; + }; + + const [device, setDevice] = useState(getDevice(NaN)); + window.addEventListener('optimizedResize', e => { + setDevice(getDevice(e && e.target && e.target.innerWidth)); + }); + return ( + <ConfigProvider device={device}> + <Shell + type="dark" + style={{ + minHeight: '100vh', + }} + > + <Shell.Branding> + <Logo + image="https://img.alicdn.com/tfs/TB1.ZBecq67gK0jSZFHXXa9jVXa-904-826.png" + text="Logo" + /> + </Shell.Branding> + <Shell.Navigation + direction="hoz" + style={{ + marginRight: 10, + }} + ></Shell.Navigation> + <Shell.Action></Shell.Action> + <Shell.Navigation> + <PageNav /> + </Shell.Navigation> + + <Shell.Content>{children}</Shell.Content> + <Shell.Footer> + <Footer /> + </Shell.Footer> + </Shell> + </ConfigProvider> + ); +} + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js new file mode 100644 index 0000000000..5332202be4 --- /dev/null +++ b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/layouts/BasicLayout/menuConfig.js @@ -0,0 +1,11 @@ + +const headerMenuConfig = []; +const asideMenuConfig = [ + { + name: 'Dashboard', + path: '/', + icon: 'smile', + }, +]; +export { headerMenuConfig, asideMenuConfig }; + \ No newline at end of file diff --git a/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.css b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.jsx b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.jsx similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.jsx rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/pages/Test/index.jsx diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/routes.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/routes.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/routes.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/routes.js diff --git a/modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/utils.js b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/utils.js similarity index 100% rename from modules/code-generator/test-cases/react-app/demo1/expected/demo-project/src/utils.js rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/src/utils.js diff --git a/modules/code-generator/test-cases/react-module/demo1/expected/demo-project/tsconfig.json b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/tsconfig.json similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/expected/demo-project/tsconfig.json rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/expected/demo-project/tsconfig.json diff --git a/modules/code-generator/test-cases/react-module/demo1/schema.json5 b/modules/code-generator/tests/fixtures/test-cases/react-module/demo1/schema.json5 similarity index 100% rename from modules/code-generator/test-cases/react-module/demo1/schema.json5 rename to modules/code-generator/tests/fixtures/test-cases/react-module/demo1/schema.json5 diff --git a/modules/code-generator/tests/plugins/jsx/__snapshots__/p0-condition-at-root.test.ts.snap b/modules/code-generator/tests/plugins/jsx/__snapshots__/p0-condition-at-root.test.ts.snap index 6de4721423..8bce3cc106 100644 --- a/modules/code-generator/tests/plugins/jsx/__snapshots__/p0-condition-at-root.test.ts.snap +++ b/modules/code-generator/tests/plugins/jsx/__snapshots__/p0-condition-at-root.test.ts.snap @@ -22,7 +22,7 @@ Object { function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { } } @@ -97,7 +97,7 @@ Object { function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { } } @@ -168,7 +168,7 @@ Object { function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { } } @@ -236,7 +236,7 @@ Object { function __$$eval(expr) { try { return expr(); - } catch (error) { + } catch (error) { } } @@ -281,3 +281,75 @@ Object { }, } `; + +exports[`condition at root invalid attr name should not be generated 1`] = ` +Object { + "chunks": Array [ + Object { + "content": " + const __$$context = this._context || this; + const { state } = __$$context; + return <Page><Text a={1}>Hello world!</Text></Page>; + ", + "fileType": "jsx", + "linkAfter": Array [ + "ReactComponentClassRenderStart", + "ReactComponentClassRenderPre", + ], + "name": "ReactComponentClassRenderJSX", + "type": "string", + }, + Object { + "content": " + function __$$eval(expr) { + try { + return expr(); + } catch (error) { + + } + } + + function __$$evalArray(expr) { + const res = __$$eval(expr); + return Array.isArray(res) ? res : []; + } + + + function __$$createChildContext(oldContext, ext) { + const childContext = { + ...oldContext, + ...ext, + }; + childContext.__proto__ = oldContext; + return childContext; + } + ", + "fileType": "jsx", + "linkAfter": Array [ + "CommonFileExport", + ], + "name": "CommonCustomContent", + "type": "string", + }, + ], + "contextData": Object {}, + "depNames": Array [], + "ir": Object { + "children": Array [ + Object { + "children": "Hello world!", + "componentName": "Text", + "props": Object { + "a": 1, + "a.b": 2, + }, + }, + ], + "componentName": "Page", + "condition": null, + "containerType": "Page", + "fileName": "test", + "moduleName": "test", + }, +} +`; diff --git a/modules/code-generator/tests/plugins/jsx/p0-condition-at-root.test.ts b/modules/code-generator/tests/plugins/jsx/p0-condition-at-root.test.ts index 984c848303..31e73d63ea 100644 --- a/modules/code-generator/tests/plugins/jsx/p0-condition-at-root.test.ts +++ b/modules/code-generator/tests/plugins/jsx/p0-condition-at-root.test.ts @@ -83,4 +83,22 @@ describe('condition at root', () => { }); expect(result).toMatchSnapshot(); }); + + test('invalid attr name should not be generated', async () => { + const containerIr: IContainerInfo = { + containerType: 'Page', + moduleName: 'test', + componentName: 'Page', + fileName: 'test', + condition: null, + children: [{ componentName: 'Text', children: 'Hello world!', props: { 'a': 1, 'a.b': 2 } }], + }; + const result = await jsx()({ + ir: containerIr, + contextData: {}, + chunks: [], + depNames: [], + }); + expect(result).toMatchSnapshot(); + }) }); diff --git a/modules/code-generator/tests/postprocessor/__snapshots__/prettier.test.ts.snap b/modules/code-generator/tests/postprocessor/__snapshots__/prettier.test.ts.snap index 18b0a2aa21..db47e1829f 100644 --- a/modules/code-generator/tests/postprocessor/__snapshots__/prettier.test.ts.snap +++ b/modules/code-generator/tests/postprocessor/__snapshots__/prettier.test.ts.snap @@ -11,7 +11,7 @@ exports[`postprocessor/prettier should works for custom files 1`] = ` `; exports[`postprocessor/prettier should works for js file 1`] = ` -"import { Button } from \\"@alifd/next\\"; +"import { Button } from '@alifd/next'; export function App() { return <Button />; } @@ -19,12 +19,17 @@ export function App() { `; exports[`postprocessor/prettier should works for json file 1`] = ` -"{ \\"components\\": [\\"Button\\", \\"Block\\"] } +"{ + \\"components\\": [ + \\"Button\\", + \\"Block\\" + ] +} " `; exports[`postprocessor/prettier should works for jsx file 1`] = ` -"import { Button } from \\"@alifd/next\\"; +"import { Button } from '@alifd/next'; export function App() { return <Button />; } diff --git a/modules/code-generator/tests/public/SchemaParser/p0-basic.test.ts b/modules/code-generator/tests/public/SchemaParser/p0-basic.test.ts index 4743e77583..311b93c6b2 100644 --- a/modules/code-generator/tests/public/SchemaParser/p0-basic.test.ts +++ b/modules/code-generator/tests/public/SchemaParser/p0-basic.test.ts @@ -1,11 +1,11 @@ -import { ProjectSchema } from '@alilc/lowcode-types'; +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { SchemaParser } from '../../../src'; import SCHEMA_WITH_SLOT from './data/schema-with-slot.json'; describe('tests/public/SchemaParser/p0-basics', () => { it('should be able to get dependencies in slots', () => { const schemaParser = new SchemaParser(); - const result = schemaParser.parse(SCHEMA_WITH_SLOT as ProjectSchema); + const result = schemaParser.parse(SCHEMA_WITH_SLOT as IPublicTypeProjectSchema); expect(result.containers.map((c) => c.deps)).toMatchSnapshot(); expect(result.containers[0].deps?.some((dep) => dep.componentName === 'Tooltip')).toBeTruthy(); expect(result.containers[0].deps?.some((dep) => dep.componentName === 'Icon')).toBeTruthy(); diff --git a/modules/code-generator/tests/public/publisher/zip/zip.test.ts b/modules/code-generator/tests/public/publisher/zip/zip.test.ts index ed37980d03..d51957004a 100644 --- a/modules/code-generator/tests/public/publisher/zip/zip.test.ts +++ b/modules/code-generator/tests/public/publisher/zip/zip.test.ts @@ -1,6 +1,14 @@ import CodeGen from '../../../../src'; +import fileSaver from 'file-saver'; +import * as utils from '../../../../src/publisher/zip/utils'; + +jest.mock('file-saver'); describe('public/publisher/zip/zip', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should works', async () => { const zip = CodeGen.publishers.zip({ outputPath: 'demo-output', @@ -19,15 +27,15 @@ describe('public/publisher/zip/zip', () => { ], }; - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"demo-output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"demo-output"'); - expect(zip.getProject()).toMatchInlineSnapshot(`undefined`); + expect(zip.getProject()).toMatchInlineSnapshot('undefined'); zip.setProject(demoProject); expect(zip.getProject()).toBeTruthy(); - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"demo-output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"demo-output"'); expect(zip.setOutputPath('output')).toBe(undefined); - expect(zip.getOutputPath()).toMatchInlineSnapshot(`"output"`); + expect(zip.getOutputPath()).toMatchInlineSnapshot('"output"'); const publishRes = await zip.publish({ project: demoProject, @@ -41,4 +49,39 @@ describe('public/publisher/zip/zip', () => { const zip = CodeGen.publishers.zip({}); expect(zip.publish()).rejects.toBeTruthy(); }); + + it('should publish the project as a zip file in the browser', async () => { + const zipContent = 'zip content'; + const zipName = 'example-project'; + jest.spyOn(utils, 'isNodeProcess').mockReturnValue(false); + // new Zip 里面也有平台判断,所以这里 mock + jest.spyOn(utils, 'generateProjectZip').mockResolvedValue(zipContent as any); + const spy = jest.spyOn(fileSaver, 'saveAs'); + + const zip = CodeGen.publishers.zip({ + projectSlug: zipName, + }); + + const demoProject = { + name: 'demo', + dirs: [], + files: [ + { + name: 'package', + ext: 'json', + content: '{ "name": "demo", "version": "1.0.0" }', + }, + ], + }; + + zip.setProject(demoProject); + const publishRes = await zip.publish({ + project: demoProject, + }); + + expect(publishRes.success).toBeTruthy(); + expect(spy).toBeCalledWith(zipContent, `${zipName}.zip`); + spy.mockReset(); + spy.mockRestore(); + }); }); diff --git a/modules/code-generator/tests/public/solutions/icejs3-app.test.ts b/modules/code-generator/tests/public/solutions/icejs3-app.test.ts new file mode 100644 index 0000000000..3d3ec4eac6 --- /dev/null +++ b/modules/code-generator/tests/public/solutions/icejs3-app.test.ts @@ -0,0 +1,63 @@ +import 'jest'; +import fs from 'fs'; +import glob from 'glob'; +import JSON from 'json5'; +import path from 'path'; + +import { + getSubDirectoriesSync, + removeActualDirRecursiveSync, + createDiskPublisher, +} from '../../helpers/solutionHelper'; + +import CodeGenerator from '../../../src'; + +import type { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; + +jest.setTimeout(15 * 1000); + +const TEST_CASES_DIR = path.join(__dirname, '../../fixtures/test-cases/icejs3-app'); +const SHOULD_UPDATE_EXPECTED = process.env.UPDATE_EXPECTED === 'true'; + +getSubDirectoriesSync(TEST_CASES_DIR).forEach(defineTest); + +function defineTest(caseDirName: string) { + test(`react-app (icejs 3)/${caseDirName} should works`, async () => { + try { + const caseFullDir = path.join(TEST_CASES_DIR, caseDirName); + const schema = JSON.parse(fs.readFileSync(path.join(caseFullDir, 'schema.json5'), 'utf-8')); + const actualDir = path.join(caseFullDir, SHOULD_UPDATE_EXPECTED ? 'expected' : 'actual'); + + removeActualDirRecursiveSync(actualDir, caseFullDir); + + await exportProject(schema, actualDir, 'demo-project'); + + const actualFiles = glob.sync('**/*.{js,jsx,json,ts,tsx,less,css,scss,sass}', { + cwd: actualDir, + }); + + expect(actualFiles.length > 0).toBeTruthy(); + + // runPrettierSync(actualFiles, actualDir); + + if (!SHOULD_UPDATE_EXPECTED) { + expect(caseFullDir).toBeSameFileContents(); + } + } catch (e) { + throw e; // just for debugger + } + }); +} + +async function exportProject(schemaJson: IPublicTypeProjectSchema, targetPath: string, projectName: string) { + const icejs3AppBuilder = CodeGenerator.solutions.icejs3(); + const result = await icejs3AppBuilder.generateProject(schemaJson); + + const publisher = createDiskPublisher(); + await publisher.publish({ + project: result, + outputPath: targetPath, + projectSlug: projectName, + createProjectFolder: true, + }); +} diff --git a/modules/code-generator/tests/public/solutions/rax-app.test.ts b/modules/code-generator/tests/public/solutions/rax-app.test.ts index 1912b7b5d4..320daf6daa 100644 --- a/modules/code-generator/tests/public/solutions/rax-app.test.ts +++ b/modules/code-generator/tests/public/solutions/rax-app.test.ts @@ -13,11 +13,11 @@ import { import CodeGenerator from '../../../src'; -import type { ProjectSchema } from '@alilc/lowcode-types'; +import type { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; jest.setTimeout(15 * 1000); -const TEST_CASES_DIR = path.join(__dirname, '../../../test-cases/rax-app'); +const TEST_CASES_DIR = path.join(__dirname, '../../fixtures/test-cases/rax-app'); const SHOULD_UPDATE_EXPECTED = process.env.UPDATE_EXPECTED === 'true'; getSubDirectoriesSync(TEST_CASES_DIR).forEach(defineTest); @@ -50,7 +50,7 @@ function defineTest(caseDirName: string) { }); } -async function exportProject(schemaJson: ProjectSchema, targetPath: string, projectName: string) { +async function exportProject(schemaJson: IPublicTypeProjectSchema, targetPath: string, projectName: string) { const raxAppBuilder = CodeGenerator.solutions.rax(); const result = await raxAppBuilder.generateProject(schemaJson); diff --git a/modules/code-generator/tests/public/solutions/react-app.test.ts b/modules/code-generator/tests/public/solutions/react-app.test.ts index bb9dc0ae5e..f70f10af09 100644 --- a/modules/code-generator/tests/public/solutions/react-app.test.ts +++ b/modules/code-generator/tests/public/solutions/react-app.test.ts @@ -12,11 +12,11 @@ import { import CodeGenerator from '../../../src'; -import type { ProjectSchema } from '@alilc/lowcode-types'; +import type { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; jest.setTimeout(15 * 1000); -const TEST_CASES_DIR = path.join(__dirname, '../../../test-cases/react-app'); +const TEST_CASES_DIR = path.join(__dirname, '../../fixtures/test-cases/react-app'); const SHOULD_UPDATE_EXPECTED = process.env.UPDATE_EXPECTED === 'true'; getSubDirectoriesSync(TEST_CASES_DIR).forEach(defineTest); @@ -49,7 +49,7 @@ function defineTest(caseDirName: string) { }); } -async function exportProject(schemaJson: ProjectSchema, targetPath: string, projectName: string) { +async function exportProject(schemaJson: IPublicTypeProjectSchema, targetPath: string, projectName: string) { const reactAppBuilder = CodeGenerator.solutions.icejs(); const result = await reactAppBuilder.generateProject(schemaJson); diff --git a/modules/code-generator/tests/utils/compositeType.test.ts b/modules/code-generator/tests/utils/compositeType.test.ts index 64ecfab9d3..40bcb170be 100644 --- a/modules/code-generator/tests/utils/compositeType.test.ts +++ b/modules/code-generator/tests/utils/compositeType.test.ts @@ -1,4 +1,5 @@ import { generateCompositeType } from '../../src/utils/compositeType'; +import { parseExpressionConvertThis2Context } from '../../src/utils/expressionParser'; import { Scope } from '../../src/utils/Scope'; test('single line string', () => { @@ -16,3 +17,125 @@ test('string with single quote', () => { test('string with double quote', () => { expect(generateCompositeType('a"bc', Scope.createRootScope())).toEqual('"a\\"bc"'); }); + +const marcoFactory = () => { + const cases: any[] = []; + + const marco = (value: any, cb: (expression: string) => void) => { + cases.push([value, cb]); + }; + + const start = () => { + test.each(cases)('parse expression %s', (item, cb) => { + const testObj = { + globalConfig: {}, + online: [ + { + description: '表格(CnTable)的数据源', + initialData: { + type: 'variable', + variable: item, + value: '', + }, + somethingelse: 'somethingelse', + }, + ], + }; + const ret = generateCompositeType(testObj, Scope.createRootScope(), { + handlers: { + function: (jsFunc) => parseExpressionConvertThis2Context(jsFunc.value, '_this'), + expression: (jsExpr) => parseExpressionConvertThis2Context(jsExpr.value, '_this'), + }, + }); + cb(ret); + }); + }; + + return { marco, start }; +}; + +const { marco: testMarco, start: startMarco } = marcoFactory(); + +/** + * dataSource 为低码编辑器里面数据源的输入 + * variable 为 schema 存储的结果 + * expect 为出码后期望生产的串 + + * |dataSource | variable | expect + * |-------------------|----------------------------|-------------- + * |"" | "\"\"" | "" + * |"helo world" | "\"hello world\"" | "hello world" + * |true | "true" | true + * |false | "false" | false + * |{"name": gaokai} | "{\"name\": \"cone\"}" | {"name": gaokai} + * | | "" | undefined + * |undefined | "undefined" | undefined + * |null | "null" | null + */ + +testMarco('""', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": \\"\\", + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('"hello world"', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": \\"hello world\\", + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('true', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": true, + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('{"name": "cone"}', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": { + \\"name\\": \\"cone\\" + }, + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": undefined, + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('undefined', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": undefined, + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +testMarco('null', (expression) => { + expect(expression).toMatchInlineSnapshot(` + "{\\"globalConfig\\": {}, + \\"online\\": [{\\"description\\": \\"表格(CnTable)的数据源\\", + \\"initialData\\": null, + \\"somethingelse\\": \\"somethingelse\\"}]}" + `); +}); + +startMarco(); diff --git a/modules/code-generator/tests/utils/expressionParser/jsExpression.test.ts b/modules/code-generator/tests/utils/expressionParser/jsExpression.test.ts new file mode 100644 index 0000000000..8a801fbe88 --- /dev/null +++ b/modules/code-generator/tests/utils/expressionParser/jsExpression.test.ts @@ -0,0 +1,45 @@ +import { generateFunction } from '../../../src/utils/jsExpression'; + +const marcoFactory = () => { + const cases: any[] = []; + + const marco = ( + value: { type: string; value: string }, + config: Record<string, string | boolean>, + expected: any, + ) => { + cases.push([value, config, expected]); + }; + + const start = () => { + test.each(cases)(`after convert this to context "${1}" should be "${3}"`, (a, b, expected) => { + expect(generateFunction(a, b)).toEqual(expected); + }); + }; + + return { marco, start }; +}; + +const { marco: testMarco, start: startMarco } = marcoFactory(); + +// 支持普通函数 +testMarco( + { + type: 'JSFunction', + value: 'function isDisabled(row, rowIndex) { \n \n}', + }, + { isArrow: true }, + '(row, rowIndex) => {}', +); + +// 支持 jsx 表达式 +testMarco( + { + type: 'JSFunction', + value: 'function content() { \n return <div>我是自定义在div内容123</div> \n}', + }, + { isArrow: true }, + '() => {\n return <div>我是自定义在div内容123</div>;\n}', +); + +startMarco(); diff --git a/modules/code-generator/tests/utils/expressionParser/parseExpressionGetKeywords.test.ts b/modules/code-generator/tests/utils/expressionParser/parseExpressionGetKeywords.test.ts new file mode 100644 index 0000000000..5f9cd700b1 --- /dev/null +++ b/modules/code-generator/tests/utils/expressionParser/parseExpressionGetKeywords.test.ts @@ -0,0 +1,31 @@ +import { parseExpressionGetKeywords } from '../../../src/utils/expressionParser'; + +const marcoFactory = () => { + const cases: any[] = []; + + const marco = (input: string | null, expected: any) => { + cases.push([input, expected]); + }; + + const start = () => { + test.each(cases)( + `after convert this to context "${1}" should be "${2}"`, + (a, expected) => { + expect(parseExpressionGetKeywords(a)).toEqual(expected); + }, + ); + }; + + return { marco, start }; +}; + +const { marco: testMarco, start: startMarco } = marcoFactory(); + +// 支持普通函数 +testMarco('function isDisabled(row) {}', []); +testMarco('function content() { \n return "hello world"\n}', []); + +// 支持 jsx 表达式 +testMarco('function content() { \n return <div>自定义在div内容123</div> \n}', []); + +startMarco(); diff --git a/modules/code-generator/tests/utils/schema/handleSubNodes.test.ts b/modules/code-generator/tests/utils/schema/handleSubNodes.test.ts index be88e3e305..d88ecdf549 100644 --- a/modules/code-generator/tests/utils/schema/handleSubNodes.test.ts +++ b/modules/code-generator/tests/utils/schema/handleSubNodes.test.ts @@ -1,10 +1,10 @@ -import { NodeData } from '@alilc/lowcode-types'; +import { IPublicTypeNodeData } from '@alilc/lowcode-types'; import { handleSubNodes } from '../../../src/utils/schema'; import SCHEMA_WITH_SLOT from './data/schema-with-slot.json'; describe('utils/schema/handleSubNodes', () => { it('should be able to visit nodes in JSSlot(1)', () => { - const nodes: NodeData[] = [ + const nodes: IPublicTypeNodeData[] = [ { componentName: 'Foo', props: { @@ -28,7 +28,7 @@ describe('utils/schema/handleSubNodes', () => { }); it('should be able to visit nodes in JSSlot(2)', () => { - const nodes: NodeData[] = (SCHEMA_WITH_SLOT as any).componentsTree[0].children; + const nodes: IPublicTypeNodeData[] = (SCHEMA_WITH_SLOT as any).componentsTree[0].children; const result = handleSubNodes(nodes, { node: (node) => node.componentName, diff --git a/modules/code-generator/tsconfig.json b/modules/code-generator/tsconfig.json index d9a27b0f22..cb0cbcb0df 100644 --- a/modules/code-generator/tsconfig.json +++ b/modules/code-generator/tsconfig.json @@ -4,5 +4,5 @@ "outDir": "lib" }, "include": ["src/**/*.ts"], - "exclude": ["./tests", "./test-cases", "../types", "node_modules"] + "exclude": ["./tests", "tests/fixtures/test-cases", "../types", "node_modules"] } diff --git a/modules/material-parser/README.md b/modules/material-parser/README.md index 4507b7e6ea..0d3f5555d5 100644 --- a/modules/material-parser/README.md +++ b/modules/material-parser/README.md @@ -4,7 +4,7 @@ 本模块负责物料接入,能自动扫描、解析源码组件,并最终产出一份符合《中后台搭建组件描述协议》的 **JSON Schema**。 -详见[文档](https://lowcode-engine.cn/docV2/yhgcqb)。 +详见[文档](https://lowcode-engine.cn/site/docs/guide/design/materialParser)。 ## demo diff --git a/modules/material-parser/package.json b/modules/material-parser/package.json index 7d7ea57c25..8b68c02955 100644 --- a/modules/material-parser/package.json +++ b/modules/material-parser/package.json @@ -66,5 +66,11 @@ }, "engines": { "node": ">=10.0.0" - } + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main/modules/material-parser" + }, + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/modules/material-parser/test/fixtures/rax-component/package.json b/modules/material-parser/test/fixtures/rax-component/package.json index 30fd28d511..2498c777fc 100644 --- a/modules/material-parser/test/fixtures/rax-component/package.json +++ b/modules/material-parser/test/fixtures/rax-component/package.json @@ -44,7 +44,6 @@ "@alife/build-plugin-lowcode": "^1.0.17", "@iceworks/spec": "^1.0.0", "@types/rax": "^1.0.0", - "build-plugin-component": "^1.0.0", "driver-universal": "^3.1.0", "eslint": "^6.8.0", "rax": "^1.1.0", diff --git a/modules/material-parser/test/fixtures/ts-component2/package.json b/modules/material-parser/test/fixtures/ts-component2/package.json index 95bb4bd8d8..a4ebf783be 100644 --- a/modules/material-parser/test/fixtures/ts-component2/package.json +++ b/modules/material-parser/test/fixtures/ts-component2/package.json @@ -34,7 +34,6 @@ "@alife/build-plugin-lowcode": "^1.0.7", "@alib/build-scripts": "^0.1.3", "@alifd/adaptor-generate": "^0.1.3", - "build-plugin-component": "^0.2.0", "build-plugin-fusion": "^0.1.0", "build-plugin-fusion-cool": "^0.1.0", "build-plugin-moment-locales": "^0.1.0", diff --git a/package.json b/package.json index 768581f5b5..213bbc1c45 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,13 @@ "lint:fix": "f2elint fix -i ./packages/*/src", "lint:modules": "f2elint scan -q -i ./modules/*/src", "lint:modules:fix": "f2elint fix -i ./modules/*/src", - "pub": "npm run watchdog:build && lerna publish patch --force-publish --exact --no-changelog", + "pub": "npm run watchdog:build && lerna publish patch --yes --force-publish --exact --no-changelog", + "pub:minor": "npm run watchdog:build && lerna publish minor --yes --force-publish --exact --no-changelog", + "pub:major": "npm run watchdog:build && lerna publish major --yes --force-publish --exact --no-changelog", "pub:premajor": "npm run watchdog:build && lerna publish premajor --force-publish --exact --dist-tag beta --preid beta --no-changelog", + "pub:preminor": "npm run watchdog:build && lerna publish preminor --force-publish --exact --dist-tag beta --preid beta --no-changelog", "pub:prepatch": "npm run watchdog:build && lerna publish prepatch --force-publish --exact --dist-tag beta --preid beta --no-changelog", - "pub:prerelease": "npm run watchdog:build && lerna publish prerelease --force-publish --exact --dist-tag beta --preid beta --no-changelog", + "pub:prerelease": "npm run watchdog:build && lerna publish prerelease --yes --force-publish --exact --dist-tag beta --preid beta --no-changelog", "setup": "node ./scripts/setup.js", "setup:test": "./scripts/setup-for-test.sh", "setup:skip-build": "./scripts/setup-skip-build.sh", @@ -31,7 +34,8 @@ "test": "lerna run test --stream", "test:snapshot": "lerna run test:snapshot", "watchdog:build": "node ./scripts/watchdog.js", - "sync": "./scripts/sync.sh" + "sync": "./scripts/sync.sh", + "syncOss": "node ./scripts/sync-oss.js" }, "husky": { "hooks": { @@ -46,9 +50,13 @@ "gulp": "^4.0.2", "husky": "^7.0.4", "lerna": "^4.0.0", - "typescript": "^4.5.5", + "typescript": "4.6.2", "yarn": "^1.22.17", - "rimraf": "^3.0.2" + "rimraf": "^3.0.2", + "@types/react-router": "5.1.18", + "@alilc/build-plugin-lce": "^0.0.5", + "babel-jest": "^26.5.2", + "@alilc/lowcode-test-mate": "^1.0.1" }, "engines": { "node": ">=14.17.0 <18" @@ -58,6 +66,8 @@ "lockfile": "enable" }, "resolutions": { - "@builder/babel-preset-ice": "1.0.1" - } + "typescript": "4.6.2", + "react-error-overlay": "6.0.9" + }, + "repository": "git@github.com:alibaba/lowcode-engine.git" } diff --git a/packages/designer/babel.config.js b/packages/designer/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/packages/designer/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/designer/build.json b/packages/designer/build.json index bd5cf18dde..3e92600554 100644 --- a/packages/designer/build.json +++ b/packages/designer/build.json @@ -1,5 +1,5 @@ { "plugins": [ - "build-plugin-component" + "@alilc/build-plugin-lce" ] } diff --git a/packages/designer/build.test.json b/packages/designer/build.test.json index 45f0dbdfd3..10d18109b8 100644 --- a/packages/designer/build.test.json +++ b/packages/designer/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ], "babelPlugins": [ diff --git a/packages/designer/jest.config.js b/packages/designer/jest.config.js index f8a7e9f982..3684a48acb 100644 --- a/packages/designer/jest.config.js +++ b/packages/designer/jest.config.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const esModules = ['zen-logger'].join('|'); +const esModules = [].join('|'); const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); const jestConfig = { @@ -10,9 +10,18 @@ const jestConfig = { // // '^.+\\.(js|jsx)$': 'babel-jest', // }, // testMatch: ['**/node-children.test.ts'], + // testMatch: ['**/plugin-manager.test.ts'], // testMatch: ['**/history/history.test.ts'], - // testMatch: ['**/plugin/plugin-manager.test.ts'], + // testMatch: ['**/document-model.test.ts'], + // testMatch: ['**/prop.test.ts'], // testMatch: ['(/tests?/.*(test))\\.[jt]s$'], + // testMatch: ['**/document/node/node.add.test.ts'], + // testMatch: ['**/setting-field.test.ts'], + // testMatch: ['**/node.test.ts'], + // 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 3f47ad0504..97256d3a21 100644 --- a/packages/designer/package.json +++ b/packages/designer/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-designer", - "version": "1.0.15", + "version": "1.3.2", "description": "Designer for Ali LowCode Engine", "main": "lib/index.js", "module": "es/index.js", @@ -9,29 +9,27 @@ "es" ], "scripts": { - "build": "build-scripts build --skip-demo", + "build": "build-scripts build", "test": "build-scripts test --config build.test.json", "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "license": "MIT", "dependencies": { - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-shell": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@alilc/lowcode-editor-core": "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", "react": "^16", "react-dom": "^16.7.0", - "semver": "^7.3.5", - "zen-logger": "^1.1.0" + "ric-shim": "^1.0.1", + "semver": "^7.3.5" }, "devDependencies": { "@alib/build-scripts": "^0.1.29", - "@alilc/lowcode-test-mate": "^1.0.1", "@testing-library/react": "^11.2.2", "@types/classnames": "^2.2.7", + "@types/enzyme": "^3.10.12", + "@types/enzyme-adapter-react-16": "^1.0.6", "@types/jest": "^26.0.16", "@types/lodash": "^4.14.165", "@types/medium-editor": "^5.0.3", @@ -39,9 +37,8 @@ "@types/react": "^16", "@types/react-dom": "^16", "@types/semver": "7.3.9", - "babel-jest": "^26.5.2", - "build-plugin-component": "^0.2.10", - "build-scripts-config": "^0.1.8", + "enzyme": "^3.11.0", + "enzyme-adapter-react-16": "^1.15.5", "jest": "^26.6.3", "lodash": "^4.17.20", "moment": "^2.29.1", @@ -51,12 +48,11 @@ "access": "public", "registry": "https://registry.npmjs.org/" }, - "resolutions": { - "@builder/babel-preset-ice": "1.0.1" - }, "repository": { "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/designer" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less b/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less index 83a411e566..8c66e85139 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less +++ b/packages/designer/src/builtin-simulator/bem-tools/bem-tools.less @@ -6,5 +6,5 @@ bottom: 0; right: 0; overflow: visible; - z-index: 800; + z-index: 1; } diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx index 63874b4ca4..212cb80bbd 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-container.tsx @@ -2,13 +2,14 @@ import * as React from 'react'; import { Component, Fragment, ReactElement, PureComponent } from 'react'; import classNames from 'classnames'; import { computed, observer, Title, globalLocale } from '@alilc/lowcode-editor-core'; -import { I18nData, isI18nData, TitleContent } from '@alilc/lowcode-types'; +import { IPublicTypeI18nData, IPublicTypeTitleContent } from '@alilc/lowcode-types'; +import { isI18nData } from '@alilc/lowcode-utils'; import { DropLocation } from '../../designer'; import { BuiltinSimulatorHost } from '../../builtin-simulator/host'; -import { ParentalNode } from '../../document/node'; +import { INode } from '../../document/node'; export class BorderContainerInstance extends PureComponent<{ - title: TitleContent; + title: IPublicTypeTitleContent; rect: DOMRect | null; scale: number; scrollX: number; @@ -36,7 +37,7 @@ export class BorderContainerInstance extends PureComponent<{ } } -function getTitle(title: string | I18nData | ReactElement) { +function getTitle(title: string | IPublicTypeI18nData | ReactElement) { if (typeof title === 'string') return title; if (isI18nData(title)) { const locale = globalLocale.getLocale() || 'zh-CN'; @@ -49,9 +50,8 @@ function getTitle(title: string | I18nData | ReactElement) { export class BorderContainer extends Component<{ host: BuiltinSimulatorHost; }, { - target?: ParentalNode; + target?: INode; }> { - state = {} as any; @computed get scale() { @@ -69,7 +69,7 @@ export class BorderContainer extends Component<{ componentDidMount() { const { host } = this.props; - host.designer.editor.on('designer.dropLocation.change', (loc: DropLocation) => { + host.designer.editor.eventBus.on('designer.dropLocation.change', (loc: DropLocation) => { let { target } = this.state; if (target === loc?.target) return; this.setState({ diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx index c0920c4506..49e68b77c8 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-detecting.tsx @@ -1,14 +1,13 @@ import { Component, Fragment, PureComponent } from 'react'; import classNames from 'classnames'; import { computed, observer, Title } from '@alilc/lowcode-editor-core'; -import { TitleContent } from '@alilc/lowcode-types'; +import { IPublicTypeTitleContent } from '@alilc/lowcode-types'; import { getClosestNode } from '@alilc/lowcode-utils'; - +import { intl } from '../../locale'; import { BuiltinSimulatorHost } from '../host'; - export class BorderDetectingInstance extends PureComponent<{ - title: TitleContent; + title: IPublicTypeTitleContent; rect: DOMRect | null; scale: number; scrollX: number; @@ -37,7 +36,7 @@ export class BorderDetectingInstance extends PureComponent<{ <div className={className} style={style}> <Title title={title} className="lc-borders-title" /> { - isLocked ? (<Title title="已锁定" className="lc-borders-status" />) : null + isLocked ? (<Title title={intl('locked')} className="lc-borders-status" />) : null } </div> ); @@ -77,11 +76,9 @@ export class BorderDetecting extends Component<{ host: BuiltinSimulatorHost }> { const { host } = this.props; const { current } = this; - - const canHoverHook = current?.componentMeta.getMetadata()?.configure.advanced?.callbacks?.onHoverHook; + const canHoverHook = current?.componentMeta.advanced.callbacks?.onHoverHook; const canHover = (canHoverHook && typeof canHoverHook === 'function') ? canHoverHook(current.internalToShellNode()) : true; - if (!canHover || !current || host.viewport.scrolling || host.liveEditing.editing) { return null; } diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx index 08a44b25d7..4b3c5c31a9 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-resizing.tsx @@ -1,10 +1,10 @@ import React, { Component, Fragment } from 'react'; import DragResizeEngine from './drag-resize-engine'; -import { observer, computed, globalContext, Editor } from '@alilc/lowcode-editor-core'; +import { observer, computed } from '@alilc/lowcode-editor-core'; import classNames from 'classnames'; import { SimulatorContext } from '../context'; import { BuiltinSimulatorHost } from '../host'; -import { OffsetObserver, Designer } from '../../designer'; +import { OffsetObserver, Designer, INode } from '../../designer'; import { Node } from '../../document'; import { normalizeTriggers } from '../../utils/misc'; @@ -135,50 +135,50 @@ export class BoxResizingInstance extends Component<{ // this.hoveringCapture.setBoundary(this.outline); this.willBind(); - const resize = (e: MouseEvent, direction: string, node: any, moveX: number, moveY: number) => { - const metadata = node.componentMeta.getMetadata(); + const resize = (e: MouseEvent, direction: string, node: INode, moveX: number, moveY: number) => { + const { advanced } = node.componentMeta; if ( - metadata.configure?.advanced?.callbacks && - typeof metadata.configure.advanced.callbacks.onResize === 'function' + advanced.callbacks && + typeof advanced.callbacks.onResize === 'function' ) { (e as any).trigger = direction; (e as any).deltaX = moveX; (e as any).deltaY = moveY; const cbNode = node?.isNode ? node.internalToShellNode() : node; - metadata.configure.advanced.callbacks.onResize(e, cbNode); + advanced.callbacks.onResize(e, cbNode); } }; - const resizeStart = (e: MouseEvent, direction: string, node: any) => { - const metadata = node.componentMeta.getMetadata(); + const resizeStart = (e: MouseEvent, direction: string, node: INode) => { + const { advanced } = node.componentMeta; if ( - metadata.configure?.advanced?.callbacks && - typeof metadata.configure.advanced.callbacks.onResizeStart === 'function' + advanced.callbacks && + typeof advanced.callbacks.onResizeStart === 'function' ) { (e as any).trigger = direction; const cbNode = node?.isNode ? node.internalToShellNode() : node; - metadata.configure.advanced.callbacks.onResizeStart(e, cbNode); + advanced.callbacks.onResizeStart(e, cbNode); } }; - const resizeEnd = (e: MouseEvent, direction: string, node: any) => { - const metadata = node.componentMeta.getMetadata(); + const resizeEnd = (e: MouseEvent, direction: string, node: INode) => { + const { advanced } = node.componentMeta; if ( - metadata.configure?.advanced?.callbacks && - typeof metadata.configure.advanced.callbacks.onResizeEnd === 'function' + advanced.callbacks && + typeof advanced.callbacks.onResizeEnd === 'function' ) { (e as any).trigger = direction; const cbNode = node?.isNode ? node.internalToShellNode() : node; - metadata.configure.advanced.callbacks.onResizeEnd(e, cbNode); + advanced.callbacks.onResizeEnd(e, cbNode); } - const editor = globalContext.get(Editor); + const editor = node.document?.designer.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.border.resize', { + editor?.eventBus.emit('designer.border.resize', { selected, layout: node?.parent?.getPropValue('layout') || '', }); @@ -235,17 +235,22 @@ export class BoxResizingInstance extends Component<{ render() { const { observed } = this.props; - if (!observed.hasOffset) { - return null; - } - - const { node, offsetWidth, offsetHeight, offsetTop, offsetLeft } = observed; let triggerVisible: any = []; - const metadata = node.componentMeta.getMetadata(); - if (metadata.configure?.advanced?.getResizingHandlers) { - triggerVisible = metadata.configure.advanced.getResizingHandlers(node.internalToShellNode()); + let offsetWidth = 0; + let offsetHeight = 0; + let offsetTop = 0; + let offsetLeft = 0; + if (observed.hasOffset) { + offsetWidth = observed.offsetWidth; + offsetHeight = observed.offsetHeight; + offsetTop = observed.offsetTop; + offsetLeft = observed.offsetLeft; + const { node } = observed; + const metadata = node.componentMeta.getMetadata(); + if (metadata.configure?.advanced?.getResizingHandlers) { + triggerVisible = metadata.configure.advanced.getResizingHandlers(node.internalToShellNode()); + } } - triggerVisible = normalizeTriggers(triggerVisible); const baseSideClass = 'lc-borders lc-resize-side'; @@ -253,90 +258,100 @@ export class BoxResizingInstance extends Component<{ return ( <div> - {triggerVisible.includes('N') && ( - <div - ref={(ref) => { this.outlineN = ref; }} - className={classNames(baseSideClass, 'n')} - style={{ - height: 20, - transform: `translate(${offsetLeft}px, ${offsetTop - 10}px)`, - width: offsetWidth, - }} - /> - )} - {triggerVisible.includes('NE') && ( - <div - ref={(ref) => { this.outlineNE = ref; }} - className={classNames(baseCornerClass, 'ne')} - style={{ - transform: `translate(${offsetLeft + offsetWidth - 5}px, ${offsetTop - 3}px)`, - cursor: 'nesw-resize', - }} - /> - )} - {triggerVisible.includes('E') && ( - <div - className={classNames(baseSideClass, 'e')} - ref={(ref) => { this.outlineE = ref; }} - style={{ - height: offsetHeight, - transform: `translate(${offsetLeft + offsetWidth - 10}px, ${offsetTop}px)`, - width: 20, - }} - /> - )} - {triggerVisible.includes('SE') && ( - <div - ref={(ref) => { this.outlineSE = ref; }} - className={classNames(baseCornerClass, 'se')} - style={{ - transform: `translate(${offsetLeft + offsetWidth - 5}px, ${offsetTop + offsetHeight - 5}px)`, - cursor: 'nwse-resize', - }} - /> - )} - {triggerVisible.includes('S') && ( - <div - ref={(ref) => { this.outlineS = ref; }} - className={classNames(baseSideClass, 's')} - style={{ - height: 20, - transform: `translate(${offsetLeft}px, ${offsetTop + offsetHeight - 10}px)`, - width: offsetWidth, - }} - /> - )} - {triggerVisible.includes('SW') && ( - <div - ref={(ref) => { this.outlineSW = ref; }} - className={classNames(baseCornerClass, 'sw')} - style={{ - transform: `translate(${offsetLeft - 3}px, ${offsetTop + offsetHeight - 5}px)`, - cursor: 'nesw-resize', - }} - /> - )} - {triggerVisible.includes('W') && ( - <div - ref={(ref) => { this.outlineW = ref; }} - className={classNames(baseSideClass, 'w')} - style={{ - height: offsetHeight, - transform: `translate(${offsetLeft - 10}px, ${offsetTop}px)`, - width: 20, - }} - /> - )} - {triggerVisible.includes('NW') && ( - <div - ref={(ref) => { this.outlineNW = ref; }} - className={classNames(baseCornerClass, 'nw')} - style={{ - transform: `translate(${offsetLeft - 3}px, ${offsetTop - 3}px)`, - cursor: 'nwse-resize', - }} - /> - )} + <div + ref={(ref) => { + this.outlineN = ref; + }} + className={classNames(baseSideClass, 'n')} + style={{ + height: 20, + transform: `translate(${offsetLeft}px, ${offsetTop - 10}px)`, + width: offsetWidth, + display: triggerVisible.includes('N') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineNE = ref; + }} + className={classNames(baseCornerClass, 'ne')} + style={{ + transform: `translate(${offsetLeft + offsetWidth - 5}px, ${offsetTop - 3}px)`, + cursor: 'nesw-resize', + display: triggerVisible.includes('NE') ? 'flex' : 'none', + }} + /> + <div + className={classNames(baseSideClass, 'e')} + ref={(ref) => { + this.outlineE = ref; + }} + style={{ + height: offsetHeight, + transform: `translate(${offsetLeft + offsetWidth - 10}px, ${offsetTop}px)`, + width: 20, + display: triggerVisible.includes('E') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineSE = ref; + }} + className={classNames(baseCornerClass, 'se')} + style={{ + transform: `translate(${offsetLeft + offsetWidth - 5}px, ${ + offsetTop + offsetHeight - 5 + }px)`, + cursor: 'nwse-resize', + display: triggerVisible.includes('SE') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineS = ref; + }} + className={classNames(baseSideClass, 's')} + style={{ + height: 20, + transform: `translate(${offsetLeft}px, ${offsetTop + offsetHeight - 10}px)`, + width: offsetWidth, + display: triggerVisible.includes('S') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineSW = ref; + }} + className={classNames(baseCornerClass, 'sw')} + style={{ + transform: `translate(${offsetLeft - 3}px, ${offsetTop + offsetHeight - 5}px)`, + cursor: 'nesw-resize', + display: triggerVisible.includes('SW') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineW = ref; + }} + className={classNames(baseSideClass, 'w')} + style={{ + height: offsetHeight, + transform: `translate(${offsetLeft - 10}px, ${offsetTop}px)`, + width: 20, + display: triggerVisible.includes('W') ? 'flex' : 'none', + }} + /> + <div + ref={(ref) => { + this.outlineNW = ref; + }} + className={classNames(baseCornerClass, 'nw')} + style={{ + transform: `translate(${offsetLeft - 3}px, ${offsetTop - 3}px)`, + cursor: 'nwse-resize', + display: triggerVisible.includes('NW') ? 'flex' : 'none', + }} + /> </div> ); } diff --git a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx index b990e045a5..143c67e020 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/border-selecting.tsx @@ -9,13 +9,13 @@ import { ComponentType, } from 'react'; import classNames from 'classnames'; -import { observer, computed, Tip, globalContext, makeObservable } from '@alilc/lowcode-editor-core'; -import { createIcon, isReactComponent } from '@alilc/lowcode-utils'; -import { ActionContentObject, isActionContentObject } from '@alilc/lowcode-types'; +import { observer, computed, Tip, engineConfig } from '@alilc/lowcode-editor-core'; +import { createIcon, isReactComponent, isActionContentObject } from '@alilc/lowcode-utils'; +import { IPublicTypeActionContentObject } from '@alilc/lowcode-types'; import { BuiltinSimulatorHost } from '../host'; -import { OffsetObserver } from '../../designer'; -import { Node } from '../../document'; +import { INode, OffsetObserver } from '../../designer'; import NodeSelector from '../node-selector'; +import { ISimulatorHost } from '../../simulator'; @observer export class BorderSelectingInstance extends Component<{ @@ -46,15 +46,19 @@ export class BorderSelectingInstance extends Component<{ dragging, }); - const hideSelectTools = observed.node.componentMeta.getMetadata().configure.advanced?.hideSelectTools; + const { hideSelectTools } = observed.node.componentMeta.advanced; + const hideComponentAction = engineConfig.get('hideComponentAction'); if (hideSelectTools) { return null; } return ( - <div className={className} style={style}> - {!dragging && <Toolbar observed={observed} />} + <div + className={className} + style={style} + > + {(!dragging && !hideComponentAction) ? <Toolbar observed={observed} /> : null} </div> ); } @@ -116,8 +120,8 @@ class Toolbar extends Component<{ observed: OffsetObserver }> { } } -function createAction(content: ReactNode | ComponentType<any> | ActionContentObject, key: string, node: Node) { - if (isValidElement(content)) { +function createAction(content: ReactNode | ComponentType<any> | IPublicTypeActionContentObject, key: string, node: INode) { + if (isValidElement<{ key: string; node: INode }>(content)) { return cloneElement(content, { key, node }); } if (isReactComponent(content)) { @@ -130,20 +134,20 @@ function createAction(content: ReactNode | ComponentType<any> | ActionContentObj key={key} className="lc-borders-action" onClick={() => { - action && action(node); - const editor = globalContext.get('editor'); + action && action(node.internalToShellNode()!); + const editor = node.document?.designer.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.border.action', { + editor?.eventBus.emit('designer.border.action', { name: key, selected, }); }} > - {icon && createIcon(icon)} + {icon && createIcon(icon, { key, node: node.internalToShellNode() })} <Tip>{title}</Tip> </div> ); @@ -152,8 +156,8 @@ function createAction(content: ReactNode | ComponentType<any> | ActionContentObj } @observer -export class BorderSelectingForNode extends Component<{ host: BuiltinSimulatorHost; node: Node }> { - get host(): BuiltinSimulatorHost { +export class BorderSelectingForNode extends Component<{ host: ISimulatorHost; node: INode }> { + get host(): ISimulatorHost { return this.props.host; } diff --git a/packages/designer/src/builtin-simulator/bem-tools/borders.less b/packages/designer/src/builtin-simulator/bem-tools/borders.less index 2cdc9fb403..cb83d36f38 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/borders.less +++ b/packages/designer/src/builtin-simulator/bem-tools/borders.less @@ -17,7 +17,7 @@ } & > &-status { margin-left: 5px; - color: #3c3c3c; + color: var(--color-text, #3c3c3c); transform: translateY(-100%); font-weight: lighter; } @@ -46,7 +46,7 @@ opacity: 1; max-height: 100%; overflow: hidden; - color: white; + color: var(--color-icon-reverse, white); &:hover { background: var(--color-brand-light, #006cff); } @@ -56,8 +56,8 @@ display: inline-block; width: 8px; height: 8px; - border: 1px solid #197aff; - background: #fff; + border: 1px solid var(--color-brand, #197aff); + background: var(--color-block-background-normal, #fff); pointer-events: auto; z-index: 2; } @@ -73,11 +73,9 @@ &:after { content: ""; display: block; - border: 1px solid #197aff; - background: #fff; - // background: #738397; + border: 1px solid var(--color-brand, #197aff); + background: var(--color-block-background-normal, #fff); border-radius: 2px; - // animation: flashing 1.5s infinite linear; } &.e, @@ -85,7 +83,6 @@ cursor: ew-resize; &:after { width: 4px; - // height: calc(100% - 20px); min-height: 50%; } } @@ -94,62 +91,24 @@ &.s { cursor: ns-resize; &:after { - // width: calc(100% - 20px); min-width: 50%; height: 4px; } } } - // &&-hovering { &&-detecting { z-index: 1; border-style: dashed; - background: rgba(0,121,242,.04); - - &.x-loop { - border-color: rgba(138, 93, 226, 0.8); - background: rgba(138, 93, 226, 0.04); - - >.@{scope}-title { - color: rgba(138, 93, 226, 1.0); - } - } - - &.x-condition { - border-color: rgba(255, 99, 8, 0.8); - background: rgba(255, 99, 8, 0.04); - >.@{scope}-title { - color: rgb(255, 99, 8); - } - } + background: var(--color-canvas-detecting-background, rgba(0,121,242,.04)); } &&-selecting { z-index: 2; border-width: 2px; - &.x-loop { - border-color: rgba(147, 112, 219, 1.0); - background: rgba(147, 112, 219, 0.04); - - >@{scope}-title { - color: rgba(147, 112, 219, 1.0); - } - &.highlight { - background: transparent; - } - } - - &.x-condition { - border-color: rgb(255, 99, 8); - >@{scope}-title { - color: rgb(255, 99, 8); - } - } - &.dragging { - background: rgba(182, 178, 178, 0.8); + background: var(--color-layer-mask-background, rgba(182, 178, 178, 0.8)); border: none; } } diff --git a/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts b/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts index d65e9b8afc..6689379dde 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts +++ b/packages/designer/src/builtin-simulator/bem-tools/drag-resize-engine.ts @@ -1,12 +1,12 @@ -import { EventEmitter } from 'events'; import { ISimulatorHost } from '../../simulator'; import { Designer, Point } from '../../designer'; import { cursor } from '@alilc/lowcode-utils'; import { makeEventsHandler } from '../../utils/misc'; +import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; // 拖动缩放 export default class DragResizeEngine { - private emitter: EventEmitter; + private emitter: IEventBus; private dragResizing = false; @@ -14,7 +14,7 @@ export default class DragResizeEngine { constructor(designer: Designer) { this.designer = designer; - this.emitter = new EventEmitter(); + this.emitter = createModuleEventBus('DragResizeEngine'); } isDragResizing() { diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.less b/packages/designer/src/builtin-simulator/bem-tools/insertion.less index 770d3893ca..fff045631a 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.less +++ b/packages/designer/src/builtin-simulator/bem-tools/insertion.less @@ -23,6 +23,6 @@ } &.invalid { - background-color: red; + background-color: var(--color-error, var(--color-function-error, red)); } } diff --git a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx index d6f748debd..98ac4266cc 100644 --- a/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx +++ b/packages/designer/src/builtin-simulator/bem-tools/insertion.tsx @@ -3,29 +3,27 @@ import { observer } from '@alilc/lowcode-editor-core'; import { BuiltinSimulatorHost } from '../host'; import { DropLocation, - Rect, - isLocationChildrenDetail, - LocationChildrenDetail, isVertical, } from '../../designer'; import { ISimulatorHost } from '../../simulator'; -import { ParentalNode } from '../../document'; +import { INode } from '../../document'; import './insertion.less'; -import { NodeData, NodeSchema } from '@alilc/lowcode-types'; +import { IPublicTypeNodeData, IPublicTypeNodeSchema, IPublicTypeLocationChildrenDetail, IPublicTypeRect } from '@alilc/lowcode-types'; +import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; interface InsertionData { edge?: DOMRect; insertType?: string; vertical?: boolean; - nearRect?: Rect; + nearRect?: IPublicTypeRect; coverRect?: DOMRect; - nearNode?: NodeData; + nearNode?: IPublicTypeNodeData; } /** * 处理拖拽子节点(INode)情况 */ -function processChildrenDetail(sim: ISimulatorHost, container: ParentalNode, detail: LocationChildrenDetail): InsertionData { +function processChildrenDetail(sim: ISimulatorHost, container: INode, detail: IPublicTypeLocationChildrenDetail): InsertionData { let edge = detail.edge || null; if (!edge) { @@ -123,7 +121,7 @@ export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> { return null; } // 如果是个绝对定位容器,不需要渲染插入标记 - if (loc.target.componentMeta.getMetadata().configure.advanced?.isAbsoluteLayoutContainer) { + if (loc.target?.componentMeta?.advanced.isAbsoluteLayoutContainer) { return null; } @@ -161,7 +159,7 @@ export class InsertionView extends Component<{ host: BuiltinSimulatorHost }> { y = ((insertType === 'before' ? nearRect.top : nearRect.bottom) + scrollY) * scale; style.width = nearRect.width * scale; } - if (y === 0 && (nearNode as NodeSchema)?.componentMeta?.isTopFixed) { + if (y === 0 && (nearNode as IPublicTypeNodeSchema)?.componentMeta?.isTopFixed) { return null; } } diff --git a/packages/designer/src/builtin-simulator/create-simulator.ts b/packages/designer/src/builtin-simulator/create-simulator.ts index 007286e9c1..57369efd89 100644 --- a/packages/designer/src/builtin-simulator/create-simulator.ts +++ b/packages/designer/src/builtin-simulator/create-simulator.ts @@ -20,8 +20,11 @@ export function createSimulator( ): Promise<BuiltinSimulatorRenderer> { const win: any = iframe.contentWindow; const doc = iframe.contentDocument!; + const innerPlugins = host.designer.editor.get('innerPlugins'); + win.AliLowCodeEngine = innerPlugins._getLowCodePluginContext({}); win.LCSimulatorHost = host; + win._ = window._; const styles: any = {}; const scripts: any = {}; @@ -53,12 +56,13 @@ export function createSimulator( } const id = asset.id ? ` data-id="${asset.id}"` : ''; const lv = asset.level || level || AssetLevel.Environment; + const scriptType = asset.scriptType ? ` type="${asset.scriptType}"` : ''; if (asset.type === AssetType.JSUrl) { scripts[lv].push( - `<script src="${asset.content}"${id}></script>`, + `<script src="${asset.content}"${id}${scriptType}></script>`, ); } else if (asset.type === AssetType.JSText) { - scripts[lv].push(`<script${id}>${asset.content}</script>`); + scripts[lv].push(`<script${id}${scriptType}>${asset.content}</script>`); } else if (asset.type === AssetType.CSSUrl) { styles[lv].push( `<link rel="stylesheet" href="${asset.content}"${id} />`, diff --git a/packages/designer/src/builtin-simulator/host-view.tsx b/packages/designer/src/builtin-simulator/host-view.tsx index ff40362d15..21e0079306 100644 --- a/packages/designer/src/builtin-simulator/host-view.tsx +++ b/packages/designer/src/builtin-simulator/host-view.tsx @@ -1,12 +1,12 @@ import React, { Component } from 'react'; -import { observer, globalContext } from '@alilc/lowcode-editor-core'; +import { observer } from '@alilc/lowcode-editor-core'; import { BuiltinSimulatorHost, BuiltinSimulatorProps } from './host'; import { BemTools } from './bem-tools'; import { Project } from '../project'; import './host.less'; /* - Simulator 模拟器,可替换部件,有协议约束, 包含画布的容器,使用场景:当 Canvas 大小变化时,用来居中处理 或 定位 Canvas + Simulator 模拟器,可替换部件,有协议约束,包含画布的容器,使用场景:当 Canvas 大小变化时,用来居中处理 或 定位 Canvas Canvas(DeviceShell) 设备壳层,通过背景图片来模拟,通过设备预设样式改变宽度、高度及定位 CanvasViewport CanvasViewport 页面编排场景中宽高不可溢出 Canvas 区 Content(Shell) 内容外层,宽高紧贴 CanvasViewport,禁用边框,禁用 margin @@ -23,8 +23,8 @@ export class BuiltinSimulatorHostView extends Component<SimulatorHostProps> { constructor(props: any) { super(props); - const { project, onMount } = this.props; - this.host = (project.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(project); + const { project, onMount, designer } = this.props; + this.host = (project.simulator as BuiltinSimulatorHost) || new BuiltinSimulatorHost(project, designer); this.host.setProps(this.props); onMount?.(this.host); } @@ -76,14 +76,14 @@ class Content extends Component<{ host: BuiltinSimulatorHost }> { private dispose?: () => void; componentDidMount() { - const editor = globalContext.get('editor'); + const editor = this.props.host.designer.editor; const onEnableEvents = (type: boolean) => { this.setState({ disabledEvents: type, }); }; - editor.on('designer.builtinSimulator.disabledEvents', onEnableEvents); + editor.eventBus.on('designer.builtinSimulator.disabledEvents', onEnableEvents); this.dispose = () => { editor.removeListener('designer.builtinSimulator.disabledEvents', onEnableEvents); @@ -97,7 +97,7 @@ class Content extends Component<{ host: BuiltinSimulatorHost }> { render() { const sim = this.props.host; const { disabledEvents } = this.state; - const { viewport } = sim; + const { viewport, designer } = sim; const frameStyle: any = { transform: `scale(${viewport.scale})`, height: viewport.contentHeight, @@ -107,10 +107,12 @@ class Content extends Component<{ host: BuiltinSimulatorHost }> { frameStyle.pointerEvents = 'none'; } + const { viewName } = designer; + return ( <div className="lc-simulator-content"> <iframe - name="SimulatorRenderer" + name={`${viewName}-SimulatorRenderer`} className="lc-simulator-content-frame" style={frameStyle} ref={(frame) => sim.mountContentFrame(frame)} diff --git a/packages/designer/src/builtin-simulator/host.less b/packages/designer/src/builtin-simulator/host.less index 5e230c4007..9a00963f24 100644 --- a/packages/designer/src/builtin-simulator/host.less +++ b/packages/designer/src/builtin-simulator/host.less @@ -31,7 +31,7 @@ max-height: calc(100% - 32px); max-width: calc(100% - 32px); transform: translateX(-50%); - box-shadow: 0 2px 10px 0 rgba(31,56,88,.15); + box-shadow: 0 2px 10px 0 var(--color-block-background-shallow, rgba(31,56,88,.15)); } &-device-iphonex { // 增加默认的小程序的壳 @@ -44,7 +44,7 @@ background: url(https://img.alicdn.com/tfs/TB1b4DHilFR4u4jSZFPXXanzFXa-750-1574.png) no-repeat top; background-size: 375px 812px; border-radius: 44px; - box-shadow: rgba(0, 0, 0, 0.1) 0 36px 42px; + box-shadow: var(--color-block-background-shallow, rgba(0, 0, 0, 0.1)) 0 36px 42px; .@{scope}-canvas-viewport { width: auto; top: 50px; @@ -73,12 +73,12 @@ } &-device-default { - top: 16px; - right: 16px; - bottom: 16px; - left: 16px; + top: var(--simulator-top-distance, 16px); + right: var(--simulator-right-distance, 16px); + bottom: var(--simulator-bottom-distance, 16px); + left: var(--simulator-left-distance, 16px); width: auto; - box-shadow: 0 1px 4px 0 rgba(31, 50, 88, 0.125); + box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 50, 88, 0.125)); } &-content { diff --git a/packages/designer/src/builtin-simulator/host.ts b/packages/designer/src/builtin-simulator/host.ts index db8625adb8..57f8569326 100644 --- a/packages/designer/src/builtin-simulator/host.ts +++ b/packages/designer/src/builtin-simulator/host.ts @@ -4,25 +4,24 @@ import { reaction, computed, getPublicPath, - hotkey, - focusTracker, engineConfig, + globalLocale, IReactionPublic, IReactionOptions, IReactionDisposer, makeObservable, + createModuleEventBus, + IEventBus, } from '@alilc/lowcode-editor-core'; -import { EventEmitter } from 'events'; + import { ISimulatorHost, Component, - NodeInstance, - ComponentInstance, DropContainer, } from '../simulator'; import Viewport from './viewport'; import { createSimulator } from './create-simulator'; -import { Node, ParentalNode, contains, isRootNode, isLowCodeComponent } from '../document'; +import { Node, INode, contains, isRootNode, isLowCodeComponent } from '../document'; import ResourceConsumer from './resource-consumer'; import { AssetLevel, @@ -36,46 +35,52 @@ import { hasOwnProperty, UtilsMetadata, getClosestNode, -} from '@alilc/lowcode-utils'; -import { - DragObjectType, - DragNodeObject, - isShaken, - LocateEvent, + transactionManager, isDragAnyObject, isDragNodeObject, isLocationData, - LocationChildrenDetail, - LocationDetailType, + Logger, +} from '@alilc/lowcode-utils'; +import { + isShaken, + ILocateEvent, isChildInline, isRowContainer, getRectTarget, - Rect, CanvasPoint, Designer, + IDesigner, } from '../designer'; import { parseMetadata } from './utils/parse-metadata'; import { getClosestClickableNode } from './utils/clickable'; import { - ComponentMetadata, - ComponentSchema, - TransformStage, - ActivityData, - Package, + IPublicTypeComponentMetadata, + IPublicTypePackage, + IPublicEnumTransitionType, + IPublicEnumDragObjectType, + IPublicTypeNodeInstance, + IPublicTypeComponentInstance, + IPublicTypeLocationChildrenDetail, + IPublicTypeLocationDetailType, + IPublicTypeRect, + IPublicModelNode, } from '@alilc/lowcode-types'; import { BuiltinSimulatorRenderer } from './renderer'; -import clipboard from '../designer/clipboard'; +import { clipboard } from '../designer/clipboard'; import { LiveEditing } from './live-editing/live-editing'; -import { Project } from '../project'; -import { Scroller } from '../designer/scroller'; +import { IProject, Project } from '../project'; +import { IScroller } from '../designer/scroller'; import { isElementNode, isDOMNodeVisible } from '../utils/misc'; +import { debounce } from 'lodash'; -export interface LibraryItem extends Package{ +const logger = new Logger({ level: 'warn', bizName: 'designer' }); + +export type LibraryItem = IPublicTypePackage & { package: string; library: string; urls?: Asset; editUrls?: Asset; -} +}; export interface DeviceStyleProps { canvas?: object; @@ -98,6 +103,7 @@ export interface BuiltinSimulatorProps { simulatorUrl?: Asset; theme?: Asset; componentsAsset?: Asset; + // eslint-disable-next-line @typescript-eslint/member-ordering [key: string]: any; } @@ -119,21 +125,6 @@ const defaultSimulatorUrl = (() => { return urls; })(); -const defaultRaxSimulatorUrl = (() => { - const publicPath = getPublicPath(); - let urls; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [_, prefix = '', dev] = /^(.+?)(\/js)?\/?$/.exec(publicPath) || []; - if (dev) { - urls = [`${prefix}/css/rax-simulator-renderer.css`, `${prefix}/js/rax-simulator-renderer.js`]; - } else if (process.env.NODE_ENV === 'production') { - urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-simulator-renderer.js`]; - } else { - urls = [`${prefix}/rax-simulator-renderer.css`, `${prefix}/rax-simulator-renderer.js`]; - } - return urls; -})(); - const defaultEnvironment = [ // https://g.alicdn.com/mylib/??react/16.11.0/umd/react.production.min.js,react-dom/16.8.6/umd/react-dom.production.min.js,prop-types/15.7.2/prop-types.min.js assetItem( @@ -148,54 +139,30 @@ const defaultEnvironment = [ ), ]; -const defaultRaxEnvironment = [ - assetItem( - AssetType.JSText, - 'window.Rax=parent.Rax;window.React=parent.React;window.ReactDOM=parent.ReactDOM;window.VisualEngineUtils=parent.VisualEngineUtils;window.VisualEngine=parent.VisualEngine', - ), - assetItem( - AssetType.JSText, - 'window.PropTypes=parent.PropTypes;React.PropTypes=parent.PropTypes; window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = window.parent.__REACT_DEVTOOLS_GLOBAL_HOOK__;', - ), -]; - export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProps> { readonly isSimulator = true; - readonly project: Project; + readonly project: IProject; - readonly designer: Designer; + readonly designer: IDesigner; readonly viewport = new Viewport(); - readonly scroller: Scroller; + readonly scroller: IScroller; - readonly emitter: EventEmitter = new EventEmitter(); + readonly emitter: IEventBus = createModuleEventBus('BuiltinSimulatorHost'); readonly componentsConsumer: ResourceConsumer; readonly injectionConsumer: ResourceConsumer; + readonly i18nConsumer: ResourceConsumer; + /** * 是否为画布自动渲染 */ autoRender = true; - constructor(project: Project) { - makeObservable(this); - this.project = project; - this.designer = project?.designer; - this.scroller = this.designer.createScroller(this.viewport); - this.autoRender = !engineConfig.get('disableAutoRender', false); - this.componentsConsumer = new ResourceConsumer<Asset | undefined>(() => this.componentsAsset); - this.injectionConsumer = new ResourceConsumer(() => { - return { - appHelper: engineConfig.get('appHelper'), - i18n: this.project.i18n, - }; - }); - } - get currentDocument() { return this.project.currentDocument; } @@ -209,7 +176,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } @computed get locale(): string { - return this.get('locale'); + return this.get('locale') || globalLocale.getLocale(); } @computed get deviceClassName(): string | undefined { @@ -228,7 +195,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return this.get('requestHandlersMap') || null; } - get thisRequiredInJSE(): any { + get thisRequiredInJSE(): boolean { return engineConfig.get('thisRequiredInJSE') ?? true; } @@ -236,6 +203,18 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return engineConfig.get('enableStrictNotFoundMode') ?? false; } + get notFoundComponent(): any { + return engineConfig.get('notFoundComponent') ?? null; + } + + get faultComponent(): any { + return engineConfig.get('faultComponent') ?? null; + } + + get faultComponentMap(): any { + return engineConfig.get('faultComponentMap') ?? null; + } + @computed get componentsAsset(): Asset | undefined { return this.get('componentsAsset'); } @@ -255,6 +234,95 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp @obx.ref _props: BuiltinSimulatorProps = {}; + @obx.ref private _contentWindow?: Window; + + get contentWindow() { + return this._contentWindow; + } + + @obx.ref private _contentDocument?: Document; + + @obx.ref private _appHelper?: any; + + get contentDocument() { + return this._contentDocument; + } + + private _renderer?: BuiltinSimulatorRenderer; + + get renderer() { + return this._renderer; + } + + readonly asyncLibraryMap: { [key: string]: {} } = {}; + + readonly libraryMap: { [key: string]: string } = {}; + + private _iframe?: HTMLIFrameElement; + + private disableHovering?: () => void; + + private disableDetecting?: () => void; + + readonly liveEditing = new LiveEditing(); + + @obx private instancesMap: { + [docId: string]: Map<string, IPublicTypeComponentInstance[]>; + } = {}; + + private tryScrollAgain: number | null = null; + + private _sensorAvailable = true; + + /** + * @see IPublicModelSensor + */ + get sensorAvailable(): boolean { + return this._sensorAvailable; + } + + private sensing = false; + + constructor(project: Project, designer: Designer) { + makeObservable(this); + this.project = project; + this.designer = designer; + this.scroller = this.designer.createScroller(this.viewport); + this.autoRender = !engineConfig.get('disableAutoRender', false); + this._appHelper = engineConfig.get('appHelper'); + this.componentsConsumer = new ResourceConsumer<Asset | undefined>(() => this.componentsAsset); + this.injectionConsumer = new ResourceConsumer(() => { + return { + appHelper: this._appHelper, + }; + }); + + engineConfig.onGot('appHelper', (data) => { + // appHelper被config.set修改后触发injectionConsumer.consume回调 + this._appHelper = data; + }); + + this.i18nConsumer = new ResourceConsumer(() => this.project.i18n); + + transactionManager.onStartTransaction(() => { + this.stopAutoRepaintNode(); + }, IPublicEnumTransitionType.REPAINT); + // 防止批量调用 transaction 时,执行多次 rerender + const rerender = debounce(this.rerender.bind(this), 28); + transactionManager.onEndTransaction(() => { + rerender(); + this.enableAutoRepaintNode(); + }, IPublicEnumTransitionType.REPAINT); + } + + stopAutoRepaintNode() { + this.renderer?.stopAutoRepaintNode(); + } + + enableAutoRepaintNode() { + this.renderer?.enableAutoRepaintNode(); + } + /** * @see ISimulator */ @@ -301,36 +369,13 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp purge(): void { // todo - } - - mountViewport(viewport: Element | null) { - this.viewport.mount(viewport); - } - - @obx.ref private _contentWindow?: Window; - - get contentWindow() { - return this._contentWindow; - } - - @obx.ref private _contentDocument?: Document; - get contentDocument() { - return this._contentDocument; } - private _renderer?: BuiltinSimulatorRenderer; - - get renderer() { - return this._renderer; + mountViewport(viewport: HTMLElement | null) { + this.viewport.mount(viewport); } - readonly asyncLibraryMap: { [key: string]: {} } = {}; - - readonly libraryMap: { [key: string]: string } = {}; - - private _iframe?: HTMLIFrameElement; - /** * { * "title":"BizCharts", @@ -342,19 +387,21 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp * ], * "library":"BizCharts" * } - * package:String 资源npm包名 - * exportName:String umd包导出名字,用于适配部分物料包define name不一致的问题,例如把BizCharts改成bizcharts,用来兼容物料用define声明的bizcharts + * package:String 资源 npm 包名 + * exportName:String umd 包导出名字,用于适配部分物料包 define name 不一致的问题,例如把 BizCharts 改成 bizcharts,用来兼容物料用 define 声明的 bizcharts * version:String 版本号 - * urls:Array 资源cdn地址,必须是umd类型,可以是.js或者.css - * library:String umd包直接导出的name + * urls:Array 资源 cdn 地址,必须是 umd 类型,可以是.js 或者.css + * library:String umd 包直接导出的 name */ buildLibrary(library?: LibraryItem[]) { const _library = library || (this.get('library') as LibraryItem[]); const libraryAsset: AssetList = []; const libraryExportList: string[] = []; + const functionCallLibraryExportList: string[] = []; if (_library && _library.length) { _library.forEach((item) => { + const { exportMode, exportSourceLibrary } = item; this.libraryMap[item.package] = item.library; if (item.async) { this.asyncLibraryMap[item.package] = item; @@ -364,6 +411,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp `Object.defineProperty(window,'${item.exportName}',{get:()=>window.${item.library}});`, ); } + if (exportMode === 'functionCall' && exportSourceLibrary) { + functionCallLibraryExportList.push( + `window["${item.library}"] = window["${exportSourceLibrary}"]("${item.library}", "${item.package}");`, + ); + } if (item.editUrls) { libraryAsset.push(item.editUrls); } else if (item.urls) { @@ -372,7 +424,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp }); } libraryAsset.unshift(assetItem(AssetType.JSText, libraryExportList.join(''))); - + libraryAsset.push(assetItem(AssetType.JSText, functionCallLibraryExportList.join(''))); return libraryAsset; } @@ -381,7 +433,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp this.renderer?.rerender?.(); } - async mountContentFrame(iframe: HTMLIFrameElement | null) { + async mountContentFrame(iframe: HTMLIFrameElement | null): Promise<void> { if (!iframe || this._iframe === iframe) { return; } @@ -392,11 +444,15 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const libraryAsset: AssetList = this.buildLibrary(); + if (this.renderEnv === 'rax') { + logger.error('After LowcodeEngine v1.3.0, Rax is no longer supported.'); + } + const vendors = [ // required & use once assetBundle( this.get('environment') || - (this.renderEnv === 'rax' ? defaultRaxEnvironment : defaultEnvironment), + defaultEnvironment, AssetLevel.Environment, ), // required & use once @@ -409,7 +465,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp // required & use once assetBundle( this.get('simulatorUrl') || - (this.renderEnv === 'rax' ? defaultRaxSimulatorUrl : defaultSimulatorUrl), + defaultSimulatorUrl, AssetLevel.Runtime, ), ]; @@ -426,8 +482,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp await this.injectionConsumer.waitFirstConsume(); if (Object.keys(this.asyncLibraryMap).length > 0) { - // 加载异步Library + // 加载异步 Library await renderer.loadAsyncLibrary(this.asyncLibraryMap); + Object.keys(this.asyncLibraryMap).forEach((key) => { + delete this.asyncLibraryMap[key]; + }); } // step 5 ready & render @@ -438,16 +497,25 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp this.setupEvents(); // bind hotkey & clipboard + const hotkey = this.designer.editor.get('innerHotkey'); hotkey.mount(this._contentWindow); - focusTracker.mount(this._contentWindow); + const innerSkeleton = this.designer.editor.get('skeleton'); + innerSkeleton.focusTracker.mount(this._contentWindow); clipboard.injectCopyPaster(this._contentDocument); // TODO: dispose the bindings } - async setupComponents(library) { + async setupComponents(library: LibraryItem[]) { const libraryAsset: AssetList = this.buildLibrary(library); - await this.renderer.load(libraryAsset); + await this.renderer?.load(libraryAsset); + if (Object.keys(this.asyncLibraryMap).length > 0) { + // 加载异步 Library + await this.renderer?.loadAsyncLibrary(this.asyncLibraryMap); + Object.keys(this.asyncLibraryMap).forEach((key) => { + delete this.asyncLibraryMap[key]; + }); + } } setupEvents() { @@ -487,19 +555,24 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return; } // FIXME: dirty fix remove label-for fro liveEditing - (downEvent.target as HTMLElement).removeAttribute('for'); - const nodeInst = this.getNodeInstanceFromElement(downEvent.target as Element); - const focusNode = documentModel.focusNode; + downEvent.target?.removeAttribute('for'); + const nodeInst = this.getNodeInstanceFromElement(downEvent.target); + const { focusNode } = documentModel; const node = getClosestClickableNode(nodeInst?.node || focusNode, downEvent); - // 如果找不到可点击的节点, 直接返回 + // 如果找不到可点击的节点,直接返回 if (!node) { return; } + // 触发 onMouseDownHook 钩子 + const onMouseDownHook = node.componentMeta.advanced.callbacks?.onMouseDownHook; + if (onMouseDownHook) { + onMouseDownHook(downEvent, node.internalToShellNode()); + } const rglNode = node?.getParent(); const isRGLNode = rglNode?.isRGLContainer; if (isRGLNode) { - // 如果拖拽的是磁铁块的右下角handle,则直接跳过 - if (downEvent.target.classList.contains('react-resizable-handle')) return; + // 如果拖拽的是磁铁块的右下角 handle,则直接跳过 + if (downEvent.target?.classList.contains('react-resizable-handle')) return; // 禁止多选 isMulti = false; designer.dragon.emitter.emit('rgl.switch', { @@ -524,11 +597,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp action: 'end', rglNode, }); - // 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除shaken检测 + // 鼠标是否移动 ? - 鼠标抖动应该也需要支持选中事件,偶尔点击不能选中,磁帖块移除 shaken 检测 if (!isShaken(downEvent, e) || isRGLNode) { let { id } = node; designer.activeTracker.track({ node, instance: nodeInst?.instance }); - if (isMulti && !node.contains(focusNode) && selection.has(id)) { + if (isMulti && focusNode && !node.contains(focusNode) && selection.has(id)) { selection.remove(id); } else { // TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式 @@ -536,7 +609,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const firstChildId = node.getChildren()?.get(0)?.getId(); if (firstChildId) id = firstChildId; } - selection.select(node.contains(focusNode) ? focusNode.id : id); + if (focusNode) { + selection.select(node.contains(focusNode) ? focusNode.id : id); + } // dirty code should refector const editor = this.designer?.editor; @@ -545,15 +620,15 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.builtinSimulator.select', { + editor?.eventBus.emit('designer.builtinSimulator.select', { selected, }); } } }; - if (isLeftButton && !node.contains(focusNode)) { - let nodes: Node[] = [node]; + if (isLeftButton && focusNode && !node.contains(focusNode)) { + let nodes: INode[] = [node]; let ignoreUpSelected = false; if (isMulti) { // multi select mode, directily add @@ -562,7 +637,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp selection.add(node.id); ignoreUpSelected = true; } - selection.remove(focusNode.id); + focusNode?.id && selection.remove(focusNode.id); // 获得顶层 nodes nodes = selection.getTopNodes(); } else if (selection.containsNode(node, true)) { @@ -572,7 +647,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } designer.dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes, }, downEvent, @@ -596,11 +671,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const x = new Event('click'); x.initEvent('click', true); this._iframe?.dispatchEvent(x); - const target = e.target as HTMLElement; + const { target } = e; const customizeIgnoreSelectors = engineConfig.get('customizeIgnoreSelectors'); // TODO: need more elegant solution to ignore click events of components in designer - const defaultIgnoreSelectors: any = [ + const defaultIgnoreSelectors: string[] = [ '.next-input-group', '.next-checkbox-group', '.next-checkbox-wrapper', @@ -636,10 +711,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp ); } - private disableHovering?: () => void; - - private disableDetecting?: () => void; - /** * 设置悬停处理 */ @@ -652,9 +723,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } const nodeInst = this.getNodeInstanceFromElement(e.target as Element); if (nodeInst?.node) { - let node = nodeInst.node; + let { node } = nodeInst; const focusNode = node.document?.focusNode; - if (node.contains(focusNode)) { + if (focusNode && node.contains(focusNode)) { node = focusNode; } detecting.capture(node); @@ -665,7 +736,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp e.stopPropagation(); } }; - const leave = () => detecting.leave(this.project.currentDocument); + const leave = () => { + this.project.currentDocument && detecting.leave(this.project.currentDocument); + }; doc.addEventListener('mouseover', hover, true); doc.addEventListener('mouseleave', leave, false); @@ -689,8 +762,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp // }; } - readonly liveEditing = new LiveEditing(); - setupLiveEditing() { const doc = this.contentDocument!; // cause edit @@ -737,7 +808,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * @see ISimulator */ - setSuspense(suspended: boolean) { + setSuspense(/** _suspended: boolean */) { return false; // if (suspended) { // /* @@ -761,22 +832,28 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp doc.addEventListener('contextmenu', (e: MouseEvent) => { const targetElement = e.target as HTMLElement; const nodeInst = this.getNodeInstanceFromElement(targetElement); + const editor = this.designer?.editor; if (!nodeInst) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } const node = nodeInst.node || this.project.currentDocument?.focusNode; if (!node) { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { + originalEvent: e, + }); return; } // dirty code should refector - const editor = this.designer?.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.builtinSimulator.contextmenu', { + editor?.eventBus.emit('designer.builtinSimulator.contextmenu', { selected, ...nodeInst, instanceRect: this.computeComponentInstanceRect(nodeInst.instance), @@ -788,7 +865,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * @see ISimulator */ - generateComponentMetadata(componentName: string): ComponentMetadata { + generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata { // if html tags if (isHTMLTag(componentName)) { return { @@ -822,15 +899,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return this.renderer?.getComponent(componentName) || null; } - createComponent(schema: ComponentSchema): Component | null { + createComponent(/** _schema: IPublicTypeComponentSchema */): Component | null { return null; // return this.renderer?.createComponent(schema) || null; } - @obx private instancesMap: { - [docId: string]: Map<string, ComponentInstance[]>; - } = {}; - setInstance(docId: string, id: string, instances: ComponentInstance[] | null) { + setInstance(docId: string, id: string, instances: IPublicTypeComponentInstance[] | null) { if (!hasOwnProperty(this.instancesMap, docId)) { this.instancesMap[docId] = new Map(); } @@ -844,8 +918,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * @see ISimulator */ - getComponentInstances(node: Node, context?: NodeInstance): ComponentInstance[] | null { - const docId = node.document.id; + getComponentInstances(node: INode, context?: IPublicTypeNodeInstance): IPublicTypeComponentInstance[] | null { + const docId = node.document?.id; + if (!docId) { + return null; + } const instances = this.instancesMap[docId]?.get(node.id) || null; if (!instances || !context) { @@ -869,16 +946,16 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp * @see ISimulator */ getClosestNodeInstance( - from: ComponentInstance, + from: IPublicTypeComponentInstance, specId?: string, - ): NodeInstance<ComponentInstance> | null { + ): IPublicTypeNodeInstance<IPublicTypeComponentInstance> | null { return this.renderer?.getClosestNodeInstance(from, specId) || null; } /** * @see ISimulator */ - computeRect(node: Node): Rect | null { + computeRect(node: INode): IPublicTypeRect | null { const instances = this.getComponentInstances(node); if (!instances) { return null; @@ -889,7 +966,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * @see ISimulator */ - computeComponentInstanceRect(instance: ComponentInstance, selector?: string): Rect | null { + computeComponentInstanceRect(instance: IPublicTypeComponentInstance, selector?: string): IPublicTypeRect | null { const renderer = this.renderer!; const elements = this.findDOMNodes(instance, selector); if (!elements) { @@ -943,7 +1020,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } if (last) { - const r: any = new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y); + const r: IPublicTypeRect = new DOMRect(last.x, last.y, last.r - last.x, last.b - last.y); r.elements = elements; r.computed = _computed; return r; @@ -955,7 +1032,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * @see ISimulator */ - findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null { + findDOMNodes(instance: IPublicTypeComponentInstance, selector?: string): Array<Element | Text> | null { const elements = this._renderer?.findDOMNodes(instance); if (!elements) { return null; @@ -974,26 +1051,24 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * 通过 DOM 节点获取节点,依赖 simulator 的接口 */ - getNodeInstanceFromElement(target: Element | null): NodeInstance<ComponentInstance> | null { + getNodeInstanceFromElement(target: Element | null): IPublicTypeNodeInstance<IPublicTypeComponentInstance, INode> | null { if (!target) { return null; } - const nodeIntance = this.getClosestNodeInstance(target); - if (!nodeIntance) { + const nodeInstance = this.getClosestNodeInstance(target); + if (!nodeInstance) { return null; } - const { docId } = nodeIntance; + const { docId } = nodeInstance; const doc = this.project.getDocument(docId)!; - const node = doc.getNode(nodeIntance.nodeId); + const node = doc.getNode(nodeInstance.nodeId); return { - ...nodeIntance, + ...nodeInstance, node, }; } - private tryScrollAgain: number | null = null; - /** * @see ISimulator */ @@ -1019,29 +1094,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp opt.top = top + scrollTop; scroll = true; } - /* - const rect = this.document.computeRect(node); - if (!rect || rect.width === 0 || rect.height === 0) { - if (!this.tryScrollAgain && tryTimes < 3) { - this.tryScrollAgain = requestAnimationFrame(() => this.scrollToNode(node, null, tryTimes + 1)); - } - return; - } - const scrollTarget = this.viewport.scrollTarget!; - const st = scrollTarget.top; - const sl = scrollTarget.left; - const { scrollHeight, scrollWidth } = scrollTarget; - const { height, width, top, bottom, left, right } = this.viewport.contentBounds; - - if (rect.height > height ? rect.top > bottom || rect.bottom < top : rect.top < top || rect.bottom > bottom) { - opt.top = Math.min(rect.top + rect.height / 2 + st - top - height / 2, scrollHeight - height); - scroll = true; - } - - if (rect.width > width ? rect.left > right || rect.right < left : rect.left < left || rect.right > right) { - opt.left = Math.min(rect.left + rect.width / 2 + sl - left - width / 2, scrollWidth - width); - scroll = true; - } */ if (scroll && this.scroller) { this.scroller.scrollTo(opt); @@ -1077,19 +1129,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp this.renderer?.clearState(); } - private _sensorAvailable = true; - /** - * @see ISensor + * @see IPublicModelSensor */ - get sensorAvailable(): boolean { - return this._sensorAvailable; - } - - /** - * @see ISensor - */ - fixEvent(e: LocateEvent): LocateEvent { + fixEvent(e: ILocateEvent): ILocateEvent { if (e.fixed) { return e; } @@ -1118,9 +1161,9 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } /** - * @see ISensor + * @see IPublicModelSensor */ - isEnter(e: LocateEvent): boolean { + isEnter(e: ILocateEvent): boolean { const rect = this.viewport.bounds; return ( e.globalY >= rect.top && @@ -1130,10 +1173,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp ); } - private sensing = false; - /** - * @see ISensor + * @see IPublicModelSensor */ deactiveSensor() { this.sensing = false; @@ -1143,17 +1184,34 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp // ========= drag location logic: helper for locate ========== /** - * @see ISensor + * @see IPublicModelSensor */ - locate(e: LocateEvent): any { + locate(e: ILocateEvent): any { const { dragObject } = e; - const { nodes } = dragObject as DragNodeObject; + + const nodes = dragObject?.nodes; const operationalNodes = nodes?.filter((node) => { - const onMoveHook = node.componentMeta?.getMetadata()?.configure.advanced?.callbacks?.onMoveHook; + const onMoveHook = node.componentMeta?.advanced.callbacks?.onMoveHook; const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node.internalToShellNode()) : true; - return canMove; + let parentContainerNode: INode | null = null; + let parentNode = node.parent; + + while (parentNode) { + if (parentNode.isContainer()) { + parentContainerNode = parentNode; + break; + } + + parentNode = parentNode.parent; + } + + const onChildMoveHook = parentContainerNode?.componentMeta?.advanced.callbacks?.onChildMoveHook; + + const childrenCanMove = onChildMoveHook && parentContainerNode && typeof onChildMoveHook === 'function' ? onChildMoveHook(node.internalToShellNode(), parentContainerNode.internalToShellNode()) : true; + + return canMove && childrenCanMove; }); if (nodes && (!operationalNodes || operationalNodes.length === 0)) { @@ -1167,12 +1225,10 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return null; } const dropContainer = this.getDropContainer(e); - const childWhitelist = dropContainer?.container?.componentMeta?.childWhitelist; - const lockedNode = getClosestNode(dropContainer?.container as Node, (node) => node.isLocked); + const lockedNode = getClosestNode(dropContainer?.container, (node) => node.isLocked); if (lockedNode) return null; if ( - !dropContainer || - (nodes && typeof childWhitelist === 'function' && !childWhitelist(operationalNodes[0])) + !dropContainer ) { return null; } @@ -1194,14 +1250,14 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const { children } = container; - const detail: LocationChildrenDetail = { - type: LocationDetailType.Children, + const detail: IPublicTypeLocationChildrenDetail = { + type: IPublicTypeLocationDetailType.Children, index: 0, edge, }; const locationData = { - target: container as ParentalNode, + target: container, detail, source: `simulator${document.id}`, event: e, @@ -1211,7 +1267,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp e.dragObject && e.dragObject.nodes && e.dragObject.nodes.length && - e.dragObject.nodes[0].componentMeta.isModal + e.dragObject.nodes[0].componentMeta.isModal && + document.focusNode ) { return this.designer.createLocation({ target: document.focusNode, @@ -1225,12 +1282,12 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return this.designer.createLocation(locationData); } - let nearRect = null; - let nearIndex = 0; - let nearNode = null; - let nearDistance = null; - let minTop = null; - let maxBottom = null; + let nearRect: IPublicTypeRect | null = null; + let nearIndex: number = 0; + let nearNode: INode | null = null; + let nearDistance: number | null = null; + let minTop: number | null = null; + let maxBottom: number | null = null; for (let i = 0, l = children.size; i < l; i++) { const node = children.get(i)!; @@ -1287,8 +1344,13 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp const vertical = inline || row; // TODO: fix type - const near: any = { - node: nearNode, + const near: { + node: IPublicModelNode; + pos: 'before' | 'after' | 'replace'; + rect?: IPublicTypeRect; + align?: 'V' | 'H'; + } = { + node: nearNode.internalToShellNode()!, pos: 'before', align: vertical ? 'V' : 'H', }; @@ -1321,13 +1383,13 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp /** * 查找合适的投放容器 */ - getDropContainer(e: LocateEvent): DropContainer | null { + getDropContainer(e: ILocateEvent): DropContainer | null { const { target, dragObject } = e; const isAny = isDragAnyObject(dragObject); const document = this.project.currentDocument!; const { currentRoot } = document; - let container: Node; - let nodeInstance: NodeInstance<ComponentInstance> | undefined; + let container: INode | null; + let nodeInstance: IPublicTypeNodeInstance<IPublicTypeComponentInstance, INode> | undefined; if (target) { const ref = this.getNodeInstanceFromElement(target); @@ -1345,8 +1407,8 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp container = currentRoot; } - if (!container.isParental()) { - container = container.parent || currentRoot; + if (!container?.isParental()) { + container = container?.parent || currentRoot; } // TODO: use spec container to accept specialData @@ -1356,7 +1418,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } // get common parent, avoid drop container contains by dragObject - const drillDownExcludes = new Set<Node>(); + const drillDownExcludes = new Set<INode>(); if (isDragNodeObject(dragObject)) { const { nodes } = dragObject; let i = nodes.length; @@ -1368,7 +1430,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } if (p !== container) { container = p || document.focusNode; - drillDownExcludes.add(container); + container && drillDownExcludes.add(container); } } @@ -1379,11 +1441,11 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } else { instance = this.getClosestNodeInstance( nodeInstance.instance as any, - container.id, + container?.id, )?.instance; } } else { - instance = this.getComponentInstances(container)?.[0]; + instance = container && this.getComponentInstances(container)?.[0]; } let dropContainer: DropContainer = { @@ -1411,54 +1473,30 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp container = container.parent; instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance; dropContainer = { - container: container as ParentalNode, + container, instance, }; } else { return null; } - } /* else if (res === DRILL_DOWN) { - if (!upward) { - container = container.parent; - instance = this.getClosestNodeInstance(dropContainer.instance, container.id)?.instance; - upward = { - container, - instance - }; - } - dropContainer = this.getNearByContainer(dropContainer, drillDownExcludes, e); - if (!dropContainer) { - dropContainer = upward; - upward = null; - } - } else if (isNode(res)) { - // TODO: - } */ + } } return null; } - isAcceptable(/* container: ParentalNode */): boolean { + isAcceptable(): boolean { return false; - /* - const meta = container.componentMeta; - const instance: any = this.document.getView(container); - if (instance && '$accept' in instance) { - return true; - } - return meta.acceptable; - */ } /** * 控制接受 */ - handleAccept({ container, instance }: DropContainer, e: LocateEvent): boolean { + handleAccept({ container }: DropContainer, e: ILocateEvent): boolean { const { dragObject } = e; const document = this.currentDocument!; - const focusNode = document.focusNode; + const { focusNode } = document; if (isRootNode(container) || container.contains(focusNode)) { - return document.checkDropTarget(focusNode, dragObject as any); + return document.checkNesting(focusNode!, dragObject as any); } const meta = (container as Node).componentMeta; @@ -1469,33 +1507,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp return false; } - // first use accept - if (acceptable) { - /* - const view: any = this.document.getView(container); - if (view && '$accept' in view) { - if (view.$accept === false) { - return false; - } - if (view.$accept === AT_CHILD || view.$accept === '@CHILD') { - return AT_CHILD; - } - if (typeof view.$accept === 'function') { - const ret = view.$accept(container, e); - if (ret || ret === false) { - return ret; - } - } - } - if (proto.acceptable) { - const ret = proto.accept(container, e); - if (ret || ret === false) { - return ret; - } - } - */ - } - // check nesting return document.checkNesting(container, dragObject as any); } @@ -1505,16 +1516,13 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp */ getNearByContainer( { container, instance }: DropContainer, - drillDownExcludes: Set<Node>, - e: LocateEvent, + drillDownExcludes: Set<INode>, ) { const { children } = container; - const document = this.project.currentDocument!; if (!children || children.isEmpty()) { return null; } - const nearDistance: any = null; const nearBy: any = null; for (let i = 0, l = children.size; i < l; i++) { let child = children.get(i); @@ -1524,7 +1532,7 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp } if (child.conditionGroup) { const bn = child.conditionGroup; - i = bn.index + bn.length - 1; + i = (bn.index || 0) + bn.length - 1; child = bn.visibleNode; } if (!child.isParental() || drillDownExcludes.has(child)) { @@ -1537,17 +1545,6 @@ export class BuiltinSimulatorHost implements ISimulatorHost<BuiltinSimulatorProp if (!rect) { continue; } - - /* - if (isPointInRect(e, rect)) { - return child; - } - - const distance = distanceToRect(e, rect); - if (nearDistance === null || distance < nearDistance) { - nearDistance = distance; - nearBy = child; - } */ } return nearBy; @@ -1559,7 +1556,7 @@ function isHTMLTag(name: string) { return /^[a-z]\w*$/.test(name); } -function isPointInRect(point: CanvasPoint, rect: Rect) { +function isPointInRect(point: CanvasPoint, rect: IPublicTypeRect) { return ( point.canvasY >= rect.top && point.canvasY <= rect.bottom && @@ -1568,7 +1565,7 @@ function isPointInRect(point: CanvasPoint, rect: Rect) { ); } -function distanceToRect(point: CanvasPoint, rect: Rect) { +function distanceToRect(point: CanvasPoint, rect: IPublicTypeRect) { let minX = Math.min(Math.abs(point.canvasX - rect.left), Math.abs(point.canvasX - rect.right)); let minY = Math.min(Math.abs(point.canvasY - rect.top), Math.abs(point.canvasY - rect.bottom)); if (point.canvasX >= rect.left && point.canvasX <= rect.right) { @@ -1581,7 +1578,7 @@ function distanceToRect(point: CanvasPoint, rect: Rect) { return Math.sqrt(minX ** 2 + minY ** 2); } -function distanceToEdge(point: CanvasPoint, rect: Rect) { +function distanceToEdge(point: CanvasPoint, rect: IPublicTypeRect) { const distanceTop = Math.abs(point.canvasY - rect.top); const distanceBottom = Math.abs(point.canvasY - rect.bottom); @@ -1591,7 +1588,7 @@ function distanceToEdge(point: CanvasPoint, rect: Rect) { }; } -function isNearAfter(point: CanvasPoint, rect: Rect, inline: boolean) { +function isNearAfter(point: CanvasPoint, rect: IPublicTypeRect, inline: boolean) { if (inline) { return ( Math.abs(point.canvasX - rect.left) + Math.abs(point.canvasY - rect.top) > diff --git a/packages/designer/src/builtin-simulator/index.ts b/packages/designer/src/builtin-simulator/index.ts index 6bcee7eb76..6977fd66c6 100644 --- a/packages/designer/src/builtin-simulator/index.ts +++ b/packages/designer/src/builtin-simulator/index.ts @@ -2,3 +2,4 @@ export * from './host'; export * from './host-view'; export * from './renderer'; export * from './live-editing/live-editing'; +export { LowcodeTypes } from './utils/parse-metadata'; diff --git a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts b/packages/designer/src/builtin-simulator/live-editing/live-editing.ts index bf3a77df13..c8594d701b 100644 --- a/packages/designer/src/builtin-simulator/live-editing/live-editing.ts +++ b/packages/designer/src/builtin-simulator/live-editing/live-editing.ts @@ -1,6 +1,6 @@ -import { obx, globalContext, Editor } from '@alilc/lowcode-editor-core'; -import { LiveTextEditingConfig } from '@alilc/lowcode-types'; -import { Node, Prop } from '../../document'; +import { obx } from '@alilc/lowcode-editor-core'; +import { IPublicTypePluginConfig, IPublicTypeLiveTextEditingConfig } from '@alilc/lowcode-types'; +import { INode, Prop } from '../../document'; const EDITOR_KEY = 'data-setter-prop'; @@ -17,7 +17,7 @@ function defaultSaveContent(content: string, prop: Prop) { } export interface EditingTarget { - node: Node; + node: INode; rootElement: HTMLElement; event: MouseEvent; } @@ -47,22 +47,26 @@ export class LiveEditing { @obx.ref private _editing: Prop | null = null; + private _dispose?: () => void; + + private _save?: () => void; + apply(target: EditingTarget) { const { node, event, rootElement } = target; const targetElement = event.target as HTMLElement; const { liveTextEditing } = node.componentMeta; - const editor = globalContext.get(Editor); + const editor = node.document?.designer.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.builtinSimulator.liveEditing', { + editor?.eventBus.emit('designer.builtinSimulator.liveEditing', { selected, }); let setterPropElement = getSetterPropElement(targetElement, rootElement); let propTarget = setterPropElement?.dataset.setterProp; - let matched: (LiveTextEditingConfig & { propElement?: HTMLElement }) | undefined | null; + let matched: (IPublicTypePluginConfig & { propElement?: HTMLElement }) | undefined | null; if (liveTextEditing) { if (propTarget) { // 已埋点命中 data-setter-prop="proptarget", 从 liveTextEditing 读取配置(mode|onSaveContent) @@ -107,7 +111,7 @@ export class LiveEditing { } // 进入编辑 - // 1. 设置contentEditable="plaintext|..." + // 1. 设置 contentEditable="plaintext|..." // 2. 添加类名 // 3. focus & cursor locate // 4. 监听 blur 事件 @@ -165,10 +169,6 @@ export class LiveEditing { return this._editing; } - private _dispose?: () => void; - - private _save?: () => void; - saveAndDispose() { if (this._save) { this._save(); @@ -186,7 +186,7 @@ export class LiveEditing { } } -export type SpecificRule = (target: EditingTarget) => (LiveTextEditingConfig & { +export type SpecificRule = (target: EditingTarget) => (IPublicTypeLiveTextEditingConfig & { propElement?: HTMLElement; }) | null; @@ -213,7 +213,6 @@ function selectRange(doc: Document, range: Range) { } } - function queryPropElement(rootElement: HTMLElement, targetElement: HTMLElement, selector?: string) { if (!selector) { return null; diff --git a/packages/designer/src/builtin-simulator/node-selector/index.less b/packages/designer/src/builtin-simulator/node-selector/index.less index c0335734e0..01552a2510 100644 --- a/packages/designer/src/builtin-simulator/node-selector/index.less +++ b/packages/designer/src/builtin-simulator/node-selector/index.less @@ -48,7 +48,7 @@ margin-top: 2px; &-content { padding-left: 6px; - background: #78869a; + background: var(--color-layer-tooltip-background, #78869a); display: inline-flex; border-radius: 3px; align-items: center; diff --git a/packages/designer/src/builtin-simulator/node-selector/index.tsx b/packages/designer/src/builtin-simulator/node-selector/index.tsx index c1e891faae..0723115da2 100644 --- a/packages/designer/src/builtin-simulator/node-selector/index.tsx +++ b/packages/designer/src/builtin-simulator/node-selector/index.tsx @@ -1,23 +1,24 @@ import { Overlay } from '@alifd/next'; -import React from 'react'; -import { Title, globalContext, Editor } from '@alilc/lowcode-editor-core'; +import React, { MouseEvent } from 'react'; +import { Title, observer } from '@alilc/lowcode-editor-core'; import { canClickNode } from '@alilc/lowcode-utils'; import './index.less'; -import { Node, ParentalNode } from '@alilc/lowcode-designer'; +import { INode } from '@alilc/lowcode-designer'; const { Popup } = Overlay; export interface IProps { - node: Node; + node: INode; } export interface IState { - parentNodes: Node[]; + parentNodes: INode[]; } -type UnionNode = Node | ParentalNode | null; +type UnionNode = INode | null; +@observer export default class InstanceNodeSelector extends React.Component<IProps, IState> { state: IState = { parentNodes: [], @@ -26,14 +27,18 @@ export default class InstanceNodeSelector extends React.Component<IProps, IState componentDidMount() { const parentNodes = this.getParentNodes(this.props.node); this.setState({ - parentNodes, + parentNodes: parentNodes ?? [], }); } - // 获取节点的父级节点(最多获取5层) - getParentNodes = (node: Node) => { + // 获取节点的父级节点(最多获取 5 层) + getParentNodes = (node: INode) => { const parentNodes: any[] = []; - const { focusNode } = node.document; + const focusNode = node.document?.focusNode; + + if (!focusNode) { + return null; + } if (node.contains(focusNode) || !focusNode.contains(node)) { return parentNodes; @@ -53,41 +58,41 @@ export default class InstanceNodeSelector extends React.Component<IProps, IState return parentNodes; }; - onSelect = (node: Node) => (e: unknown) => { + onSelect = (node: INode) => (event: MouseEvent) => { if (!node) { return; } - const canClick = canClickNode(node, e as MouseEvent); + const canClick = canClickNode(node.internalToShellNode()!, event); if (canClick && typeof node.select === 'function') { node.select(); - const editor = globalContext.get(Editor); + const editor = node.document?.designer.editor; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit('designer.border.action', { + editor?.eventBus.emit('designer.border.action', { name: 'select', selected, }); } }; - onMouseOver = (node: Node) => (_: any, flag = true) => { + onMouseOver = (node: INode) => (_: any, flag = true) => { if (node && typeof node.hover === 'function') { node.hover(flag); } }; - onMouseOut = (node: Node) => (_: any, flag = false) => { + onMouseOut = (node: INode) => (_: any, flag = false) => { if (node && typeof node.hover === 'function') { node.hover(flag); } }; - renderNodes = (/* node: Node */) => { + renderNodes = () => { const nodes = this.state.parentNodes; if (!nodes || nodes.length < 1) { return null; @@ -135,7 +140,7 @@ export default class InstanceNodeSelector extends React.Component<IProps, IState triggerType="hover" offset={[0, 0]} > - <div className="instance-node-selector">{this.renderNodes(node)}</div> + <div className="instance-node-selector">{this.renderNodes()}</div> </Popup> </div> ); diff --git a/packages/designer/src/builtin-simulator/renderer.ts b/packages/designer/src/builtin-simulator/renderer.ts index 7f0934252a..15664757bc 100644 --- a/packages/designer/src/builtin-simulator/renderer.ts +++ b/packages/designer/src/builtin-simulator/renderer.ts @@ -1,19 +1,7 @@ -import { ComponentInstance, NodeInstance, Component } from '../simulator'; -import { NodeSchema } from '@alilc/lowcode-types'; +import { Component } from '../simulator'; +import { IPublicTypeComponentInstance, IPublicTypeSimulatorRenderer } from '@alilc/lowcode-types'; -export interface BuiltinSimulatorRenderer { - readonly isSimulatorRenderer: true; - createComponent(schema: NodeSchema): Component | null; - getComponent(componentName: string): Component; - getClosestNodeInstance(from: ComponentInstance, nodeId?: string): NodeInstance<ComponentInstance> | null; - findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null; - getClientRects(element: Element | Text): DOMRect[]; - setNativeSelection(enableFlag: boolean): void; - setDraggingState(state: boolean): void; - setCopyState(state: boolean): void; - clearState(): void; - run(): void; -} +export type BuiltinSimulatorRenderer = IPublicTypeSimulatorRenderer<Component, IPublicTypeComponentInstance>; export function isSimulatorRenderer(obj: any): obj is BuiltinSimulatorRenderer { return obj && obj.isSimulatorRenderer; diff --git a/packages/designer/src/builtin-simulator/resource-consumer.ts b/packages/designer/src/builtin-simulator/resource-consumer.ts index 20d1883eae..cc195e516f 100644 --- a/packages/designer/src/builtin-simulator/resource-consumer.ts +++ b/packages/designer/src/builtin-simulator/resource-consumer.ts @@ -1,6 +1,5 @@ -import { autorun, obx } from '@alilc/lowcode-editor-core'; +import { autorun, makeObservable, obx, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; import { BuiltinSimulatorHost } from './host'; -import { EventEmitter } from 'events'; import { BuiltinSimulatorRenderer, isSimulatorRenderer } from './renderer'; const UNSET = Symbol('unset'); @@ -20,7 +19,7 @@ export type RendererConsumer<T> = (renderer: BuiltinSimulatorRenderer, data: T) // 2. 消费机制(渲染进程自定 + 传递进入) export default class ResourceConsumer<T = any> { - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('ResourceConsumer'); @obx.ref private _data: T | typeof UNSET = UNSET; @@ -28,7 +27,12 @@ export default class ResourceConsumer<T = any> { private _consuming?: () => void; + private _firstConsumed = false; + + private resolveFirst?: (resolve?: any) => void; + constructor(provider: () => T, private consumer?: RendererConsumer<T>) { + makeObservable(this); this._providing = autorun(() => { this._data = provider(); }); @@ -46,7 +50,7 @@ export default class ResourceConsumer<T = any> { } const rendererConsumer = this.consumer!; - consumer = data => rendererConsumer(consumerOrRenderer, data); + consumer = (data) => rendererConsumer(consumerOrRenderer, data); } else { consumer = consumerOrRenderer; } @@ -56,8 +60,8 @@ export default class ResourceConsumer<T = any> { } await consumer(this._data); // TODO: catch error and report - if (this.resovleFirst) { - this.resovleFirst(); + if (this.resolveFirst) { + this.resolveFirst(); } else { this._firstConsumed = true; } @@ -74,16 +78,12 @@ export default class ResourceConsumer<T = any> { this.emitter.removeAllListeners(); } - private _firstConsumed = false; - - private resovleFirst?: () => void; - waitFirstConsume(): Promise<any> { if (this._firstConsumed) { return Promise.resolve(); } - return new Promise(resolve => { - this.resovleFirst = resolve; + return new Promise((resolve) => { + this.resolveFirst = resolve; }); } } diff --git a/packages/designer/src/builtin-simulator/utils/clickable.ts b/packages/designer/src/builtin-simulator/utils/clickable.ts index bc9d8f349b..5413ad5c52 100644 --- a/packages/designer/src/builtin-simulator/utils/clickable.ts +++ b/packages/designer/src/builtin-simulator/utils/clickable.ts @@ -1,5 +1,5 @@ import { getClosestNode, canClickNode } from '@alilc/lowcode-utils'; -import { Node } from '../../document'; +import { INode } from '../../document'; /** * 获取离当前节点最近的可点击节点 @@ -7,7 +7,7 @@ import { Node } from '../../document'; * @param event */ export const getClosestClickableNode = ( - currentNode: Node | undefined | null, + currentNode: INode | undefined | null, event: MouseEvent, ) => { let node = currentNode; @@ -25,7 +25,7 @@ export const getClosestClickableNode = ( if (canClick) { break; } - // 对于不可点击的节点, 继续向上找 + // 对于不可点击的节点,继续向上找 node = node.parent; } return node; diff --git a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts b/packages/designer/src/builtin-simulator/utils/parse-metadata.ts index ca0a6071c3..6969a47db5 100644 --- a/packages/designer/src/builtin-simulator/utils/parse-metadata.ts +++ b/packages/designer/src/builtin-simulator/utils/parse-metadata.ts @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import { isValidElement } from 'react'; import { isElement } from '@alilc/lowcode-utils'; -import { PropConfig } from '@alilc/lowcode-types'; +import { IPublicTypePropConfig } from '@alilc/lowcode-types'; export const primitiveTypes = [ 'string', @@ -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; } @@ -46,16 +54,18 @@ function define(propType: any = PropTypes.any, lowcodeType: string | object = {} return lowcodeCheckType; } -const LowcodeTypes: any = { +export const LowcodeTypes: any = { ...PropTypes, define, }; (window as any).PropTypes = LowcodeTypes; -(window as any).React.PropTypes = LowcodeTypes; +if ((window as any).React) { + (window as any).React.PropTypes = LowcodeTypes; +} // override primitive type checkers -primitiveTypes.forEach(type => { +primitiveTypes.forEach((type) => { const propType = (PropTypes as any)[type]; if (!propType) { return; @@ -91,7 +101,7 @@ LowcodeTypes.objectOf = (type: any) => { // An object that could be one of many types LowcodeTypes.oneOfType = (types: any[]) => { - const itemTypes = types.map(type => type.lowcodeType || 'any'); + const itemTypes = types.map((type) => type.lowcodeType || 'any'); return define(PropTypes.oneOfType(types), { type: 'oneOfType', value: itemTypes, @@ -100,7 +110,7 @@ LowcodeTypes.oneOfType = (types: any[]) => { // An object with warnings on extra properties LowcodeTypes.exact = (typesMap: any) => { - const configs = Object.keys(typesMap).map(key => { + const configs = Object.keys(typesMap).map((key) => { return { name: key, propType: typesMap[key]?.lowcodeType || 'any', @@ -113,8 +123,8 @@ LowcodeTypes.exact = (typesMap: any) => { }; // An object taking on a particular shape -LowcodeTypes.shape = (typesMap: any) => { - const configs = Object.keys(typesMap).map(key => { +LowcodeTypes.shape = (typesMap: any = {}) => { + const configs = Object.keys(typesMap).map((key) => { return { name: key, propType: typesMap[key]?.lowcodeType || 'any', @@ -127,7 +137,7 @@ LowcodeTypes.shape = (typesMap: any) => { }; const BasicTypes = ['string', 'number', 'object']; -export function parseProps(component: any): PropConfig[] { +export function parseProps(component: any): IPublicTypePropConfig[] { if (!component) { return []; } @@ -135,7 +145,7 @@ export function parseProps(component: any): PropConfig[] { const defaultProps = component.defaultProps || ({} as any); const result: any = {}; if (!propTypes) return []; - Object.keys(propTypes).forEach(key => { + Object.keys(propTypes).forEach((key) => { const propTypeItem = propTypes[key]; const defaultValue = defaultProps[key]; const { lowcodeType } = propTypeItem; @@ -173,7 +183,7 @@ export function parseProps(component: any): PropConfig[] { } }); - Object.keys(defaultProps).forEach(key => { + Object.keys(defaultProps).forEach((key) => { if (result[key]) return; const defaultValue = defaultProps[key]; let type: string = typeof defaultValue; @@ -198,7 +208,7 @@ export function parseProps(component: any): PropConfig[] { }; }); - return Object.keys(result).map(key => result[key]); + return Object.keys(result).map((key) => result[key]); } export function parseMetadata(component: any): any { diff --git a/packages/designer/src/builtin-simulator/utils/path.ts b/packages/designer/src/builtin-simulator/utils/path.ts index 49f0c71afb..835c5a20bc 100644 --- a/packages/designer/src/builtin-simulator/utils/path.ts +++ b/packages/designer/src/builtin-simulator/utils/path.ts @@ -13,7 +13,7 @@ export function isPackagePath(path: string): boolean { export function toTitleCase(s: string): string { return s .split(/[-_ .]+/) - .map(token => token[0].toUpperCase() + token.substring(1)) + .map((token) => token[0].toUpperCase() + token.substring(1)) .join(''); } diff --git a/packages/designer/src/component-actions.ts b/packages/designer/src/component-actions.ts new file mode 100644 index 0000000000..57ad30bf25 --- /dev/null +++ b/packages/designer/src/component-actions.ts @@ -0,0 +1,163 @@ +import { IPublicModelNode, IPublicTypeComponentAction, IPublicTypeMetadataTransducer } from '@alilc/lowcode-types'; +import { engineConfig } from '@alilc/lowcode-editor-core'; +import { intlNode } from './locale'; +import { + IconLock, + IconUnlock, + IconRemove, + IconClone, + IconHidden, +} from './icons'; +import { componentDefaults, legacyIssues } from './transducers'; + +function deduplicateRef(node: IPublicModelNode | null | undefined) { + const currentRef = node?.getPropValue('ref'); + if (currentRef) { + node?.setPropValue('ref', `${node.componentName.toLowerCase()}-${Math.random().toString(36).slice(2, 9)}`); + } + node?.children?.forEach(deduplicateRef); +} + +export class ComponentActions { + private metadataTransducers: IPublicTypeMetadataTransducer[] = []; + + actions: IPublicTypeComponentAction[] = [ + { + name: 'remove', + content: { + icon: IconRemove, + title: intlNode('remove'), + /* istanbul ignore next */ + action(node: IPublicModelNode) { + node.remove(); + }, + }, + important: true, + }, + { + name: 'hide', + content: { + icon: IconHidden, + title: intlNode('hide'), + /* istanbul ignore next */ + action(node: IPublicModelNode) { + node.visible = false; + }, + }, + /* istanbul ignore next */ + condition: (node: IPublicModelNode) => { + return node.componentMeta?.isModal; + }, + important: true, + }, + { + name: 'copy', + content: { + icon: IconClone, + title: intlNode('copy'), + /* istanbul ignore next */ + action(node: IPublicModelNode) { + // node.remove(); + const { document: doc, parent, index } = node; + if (parent) { + const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true); + deduplicateRef(newNode); + newNode?.select(); + const { isRGL, rglNode } = node?.getRGL(); + if (isRGL) { + // 复制 layout 信息 + const layout: any = rglNode?.getPropValue('layout') || []; + const curLayout = layout.filter((item: any) => item.i === node.getPropValue('fieldId')); + if (curLayout && curLayout[0]) { + layout.push({ + ...curLayout[0], + i: newNode?.getPropValue('fieldId'), + }); + rglNode?.setPropValue('layout', layout); + // 如果是磁贴块复制,则需要滚动到影响位置 + setTimeout(() => newNode?.document?.project?.simulatorHost?.scrollToNode(newNode), 10); + } + } + } + }, + }, + important: true, + }, + { + name: 'lock', + content: { + icon: IconLock, // 锁定 icon + title: intlNode('lock'), + /* istanbul ignore next */ + action(node: IPublicModelNode) { + node.lock(); + }, + }, + /* istanbul ignore next */ + condition: (node: IPublicModelNode) => { + return engineConfig.get('enableCanvasLock', false) && node.isContainerNode && !node.isLocked; + }, + important: true, + }, + { + name: 'unlock', + content: { + icon: IconUnlock, // 解锁 icon + title: intlNode('unlock'), + /* istanbul ignore next */ + action(node: IPublicModelNode) { + node.lock(false); + }, + }, + /* istanbul ignore next */ + condition: (node: IPublicModelNode) => { + return engineConfig.get('enableCanvasLock', false) && node.isContainerNode && node.isLocked; + }, + important: true, + }, + ]; + + constructor() { + this.registerMetadataTransducer(legacyIssues, 2, 'legacy-issues'); // should use a high level priority, eg: 2 + this.registerMetadataTransducer(componentDefaults, 100, 'component-defaults'); + } + + removeBuiltinComponentAction(name: string) { + const i = this.actions.findIndex((action) => action.name === name); + if (i > -1) { + this.actions.splice(i, 1); + } + } + addBuiltinComponentAction(action: IPublicTypeComponentAction) { + this.actions.push(action); + } + + modifyBuiltinComponentAction( + actionName: string, + handle: (action: IPublicTypeComponentAction) => void, + ) { + const builtinAction = this.actions.find((action) => action.name === actionName); + if (builtinAction) { + handle(builtinAction); + } + } + + registerMetadataTransducer( + transducer: IPublicTypeMetadataTransducer, + level = 100, + id?: string, + ) { + transducer.level = level; + transducer.id = id; + const i = this.metadataTransducers.findIndex((item) => item.level != null && item.level > level); + if (i < 0) { + this.metadataTransducers.push(transducer); + } else { + this.metadataTransducers.splice(i, 0, transducer); + } + } + + getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[] { + return this.metadataTransducers; + } +} \ No newline at end of file diff --git a/packages/designer/src/component-meta.ts b/packages/designer/src/component-meta.ts index 4e1a74c77d..1ee1154f18 100644 --- a/packages/designer/src/component-meta.ts +++ b/packages/designer/src/component-meta.ts @@ -1,34 +1,27 @@ import { ReactElement } from 'react'; import { - ComponentMetadata, - NpmInfo, - NodeData, - NodeSchema, - ComponentAction, - TitleContent, - TransformedComponentMetadata, - NestingFilter, - isTitleConfig, - I18nData, - LiveTextEditingConfig, - FieldConfig, + IPublicTypeComponentMetadata, + IPublicTypeNpmInfo, + IPublicTypeNodeData, + IPublicTypeNodeSchema, + IPublicTypeTitleContent, + IPublicTypeTransformedComponentMetadata, + IPublicTypeNestingFilter, + IPublicTypeI18nData, + IPublicTypeFieldConfig, + IPublicModelComponentMeta, + IPublicTypeAdvanced, + IPublicTypeDisposable, + IPublicTypeLiveTextEditingConfig, } from '@alilc/lowcode-types'; -import { deprecate, isRegExp } from '@alilc/lowcode-utils'; -import { computed, engineConfig } from '@alilc/lowcode-editor-core'; -import EventEmitter from 'events'; -import { componentDefaults, legacyIssues } from './transducers'; -import { isNode, Node, ParentalNode } from './document'; +import { deprecate, isRegExp, isTitleConfig, isNode } from '@alilc/lowcode-utils'; +import { computed, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; +import { Node, INode } from './document'; import { Designer } from './designer'; -import { intlNode } from './locale'; import { - IconLock, - IconUnlock, IconContainer, IconPage, IconComponent, - IconRemove, - IconClone, - IconHidden, } from './icons'; export function ensureAList(list?: string | string[]): string[] | null { @@ -47,7 +40,7 @@ export function ensureAList(list?: string | string[]): string[] | null { return list; } -export function buildFilter(rule?: string | string[] | RegExp | NestingFilter) { +export function buildFilter(rule?: string | string[] | RegExp | IPublicTypeNestingFilter) { if (!rule) { return null; } @@ -55,21 +48,37 @@ export function buildFilter(rule?: string | string[] | RegExp | NestingFilter) { return rule; } if (isRegExp(rule)) { - return (testNode: Node | NodeSchema) => rule.test(testNode.componentName); + return (testNode: Node | IPublicTypeNodeSchema) => { + return rule.test(testNode.componentName); + }; } const list = ensureAList(rule); if (!list) { return null; } - return (testNode: Node | NodeSchema) => list.includes(testNode.componentName); + return (testNode: Node | IPublicTypeNodeSchema) => { + return list.includes(testNode.componentName); + }; +} + +export interface IComponentMeta extends IPublicModelComponentMeta<INode> { + prototype?: any; + + liveTextEditing?: IPublicTypeLiveTextEditingConfig[]; + + get rootSelector(): string | undefined; + + setMetadata(metadata: IPublicTypeComponentMetadata): void; + + onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable; } -export class ComponentMeta { +export class ComponentMeta implements IComponentMeta { readonly isComponentMeta = true; - private _npm?: NpmInfo; + private _npm?: IPublicTypeNpmInfo; - private emitter: EventEmitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('ComponentMeta'); get npm() { return this._npm; @@ -113,14 +122,14 @@ export class ComponentMeta { return this._rootSelector; } - private _transformedMetadata?: TransformedComponentMetadata; + private _transformedMetadata?: IPublicTypeTransformedComponentMetadata; - get configure() { + get configure(): IPublicTypeFieldConfig[] { const config = this._transformedMetadata?.configure; return config?.combined || config?.props || []; } - private _liveTextEditing?: LiveTextEditingConfig[]; + private _liveTextEditing?: IPublicTypeLiveTextEditingConfig[]; get liveTextEditing() { return this._liveTextEditing; @@ -128,23 +137,23 @@ export class ComponentMeta { private _isTopFixed?: boolean; - get isTopFixed() { - return this._isTopFixed; + get isTopFixed(): boolean { + return !!(this._isTopFixed); } - private parentWhitelist?: NestingFilter | null; + private parentWhitelist?: IPublicTypeNestingFilter | null; - private childWhitelist?: NestingFilter | null; + private childWhitelist?: IPublicTypeNestingFilter | null; - private _title?: TitleContent; + private _title?: IPublicTypeTitleContent; private _isMinimalRenderUnit?: boolean; - get title(): string | I18nData | ReactElement { + get title(): string | IPublicTypeI18nData | ReactElement { // string | i18nData | ReactElement // TitleConfig title.label if (isTitleConfig(this._title)) { - return (this._title.label as any) || this.componentName; + return (this._title?.label as any) || this.componentName; } return this._title || this.componentName; } @@ -165,19 +174,32 @@ export class ComponentMeta { return this._acceptable!; } - constructor(readonly designer: Designer, metadata: ComponentMetadata) { + get advanced(): IPublicTypeAdvanced { + return this.getMetadata().configure.advanced || {}; + } + + /** + * @legacy compatiable for vision + * @deprecated + */ + prototype?: any; + + constructor(readonly designer: Designer, metadata: IPublicTypeComponentMetadata) { this.parseMetadata(metadata); } - setNpm(info: NpmInfo) { + setNpm(info: IPublicTypeNpmInfo) { if (!this._npm) { this._npm = info; } } - private parseMetadata(metadata: ComponentMetadata) { + private parseMetadata(metadata: IPublicTypeComponentMetadata) { const { componentName, npm, ...others } = metadata; let _metadata = metadata; + if ((metadata as any).prototype) { + this.prototype = (metadata as any).prototype; + } if (!npm && !Object.keys(others).length) { // 没有注册的组件,只能删除,不支持复制、移动等操作 _metadata = { @@ -212,9 +234,9 @@ export class ComponentMeta { : title; } - const liveTextEditing = this._transformedMetadata.configure.advanced?.liveTextEditing || []; + const liveTextEditing = this.advanced.liveTextEditing || []; - function collectLiveTextEditing(items: FieldConfig[]) { + function collectLiveTextEditing(items: IPublicTypeFieldConfig[]) { items.forEach((config) => { if (config?.items) { collectLiveTextEditing(config.items); @@ -232,7 +254,7 @@ export class ComponentMeta { collectLiveTextEditing(this.configure); this._liveTextEditing = liveTextEditing.length > 0 ? liveTextEditing : undefined; - const isTopFixed = this._transformedMetadata.configure.advanced?.isTopFixed; + const isTopFixed = this.advanced.isTopFixed; if (isTopFixed) { this._isTopFixed = isTopFixed; @@ -264,8 +286,11 @@ export class ComponentMeta { this.parseMetadata(this.getMetadata()); } - private transformMetadata(metadta: ComponentMetadata): TransformedComponentMetadata { - const result = getRegisteredMetadataTransducers().reduce((prevMetadata, current) => { + private transformMetadata( + metadta: IPublicTypeComponentMetadata, + ): IPublicTypeTransformedComponentMetadata { + const registeredTransducers = this.designer.componentActions.getRegisteredMetadataTransducers(); + const result = registeredTransducers.reduce((prevMetadata, current) => { return current(prevMetadata); }, preprocessMetadata(metadta)); @@ -279,7 +304,7 @@ export class ComponentMeta { return result as any; } - isRootComponent(includeBlock = true) { + isRootComponent(includeBlock = true): boolean { return ( this.componentName === 'Page' || this.componentName === 'Component' || @@ -293,7 +318,7 @@ export class ComponentMeta { const disabled = ensureAList(disableBehaviors) || (this.isRootComponent(false) ? ['copy', 'remove', 'lock', 'unlock'] : null); - actions = builtinComponentActions.concat( + actions = this.designer.componentActions.actions.concat( this.designer.getGlobalComponentActions() || [], actions || [], ); @@ -307,31 +332,31 @@ export class ComponentMeta { return actions; } - setMetadata(metadata: ComponentMetadata) { + setMetadata(metadata: IPublicTypeComponentMetadata) { this.parseMetadata(metadata); } - getMetadata(): TransformedComponentMetadata { + getMetadata(): IPublicTypeTransformedComponentMetadata { return this._transformedMetadata!; } - checkNestingUp(my: Node | NodeData, parent: ParentalNode) { + checkNestingUp(my: INode | IPublicTypeNodeData, parent: INode) { // 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器 if (this.parentWhitelist) { return this.parentWhitelist( parent.internalToShellNode(), - isNode(my) ? my.internalToShellNode() : my, + isNode<INode>(my) ? my.internalToShellNode() : my, ); } return true; } - checkNestingDown(my: Node, target: Node | NodeSchema | NodeSchema[]) { + checkNestingDown(my: INode, target: INode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[]): boolean { // 检查父子关系,直接约束型,在画布中拖拽直接掠过目标容器 if (this.childWhitelist) { const _target: any = !Array.isArray(target) ? [target] : target; - return _target.every((item: Node | NodeSchema) => { - const _item = !isNode(item) ? new Node(my.document, item) : item; + return _target.every((item: Node | IPublicTypeNodeSchema) => { + const _item = !isNode<INode>(item) ? new Node(my.document, item) : item; return ( this.childWhitelist && this.childWhitelist(_item.internalToShellNode(), my.internalToShellNode()) @@ -341,22 +366,20 @@ export class ComponentMeta { return true; } - onMetadataChange(fn: (args: any) => void): () => void { + onMetadataChange(fn: (args: any) => void): IPublicTypeDisposable { this.emitter.on('metadata_change', fn); return () => { this.emitter.removeListener('metadata_change', fn); }; } - // compatiable vision - prototype?: any; } export function isComponentMeta(obj: any): obj is ComponentMeta { return obj && obj.isComponentMeta; } -function preprocessMetadata(metadata: ComponentMetadata): TransformedComponentMetadata { +function preprocessMetadata(metadata: IPublicTypeComponentMetadata): IPublicTypeTransformedComponentMetadata { if (metadata.configure) { if (Array.isArray(metadata.configure)) { return { @@ -374,155 +397,3 @@ function preprocessMetadata(metadata: ComponentMetadata): TransformedComponentMe configure: {}, }; } - -export interface MetadataTransducer { - (prev: TransformedComponentMetadata): TransformedComponentMetadata; - /** - * 0 - 9 system - * 10 - 99 builtin-plugin - * 100 - app & plugin - */ - level?: number; - /** - * use to replace TODO - */ - id?: string; -} -const metadataTransducers: MetadataTransducer[] = []; - -export function registerMetadataTransducer( - transducer: MetadataTransducer, - level = 100, - id?: string, -) { - transducer.level = level; - transducer.id = id; - const i = metadataTransducers.findIndex((item) => item.level != null && item.level > level); - if (i < 0) { - metadataTransducers.push(transducer); - } else { - metadataTransducers.splice(i, 0, transducer); - } -} - -export function getRegisteredMetadataTransducers(): MetadataTransducer[] { - return metadataTransducers; -} - -const builtinComponentActions: ComponentAction[] = [ - { - name: 'remove', - content: { - icon: IconRemove, - title: intlNode('remove'), - /* istanbul ignore next */ - action(node: Node) { - node.remove(); - }, - }, - important: true, - }, - { - name: 'hide', - content: { - icon: IconHidden, - title: intlNode('hide'), - /* istanbul ignore next */ - action(node: Node) { - node.setVisible(false); - }, - }, - /* istanbul ignore next */ - condition: (node: Node) => { - return node.componentMeta.isModal; - }, - important: true, - }, - { - name: 'copy', - content: { - icon: IconClone, - title: intlNode('copy'), - /* istanbul ignore next */ - action(node: Node) { - // node.remove(); - const { document: doc, parent, index } = node; - if (parent) { - const newNode = doc.insertNode(parent, node, index + 1, true); - newNode.select(); - const { isRGL, rglNode } = node.getRGL(); - if (isRGL) { - // 复制layout信息 - let layout = rglNode.getPropValue('layout') || []; - let curLayout = layout.filter((item) => item.i === node.getPropValue('fieldId')); - if (curLayout && curLayout[0]) { - layout.push({ - ...curLayout[0], - i: newNode.getPropValue('fieldId'), - }); - rglNode.setPropValue('layout', layout); - // 如果是磁贴块复制,则需要滚动到影响位置 - setTimeout(() => newNode.document.simulator?.scrollToNode(newNode), 10); - } - } - } - }, - }, - important: true, - }, - { - name: 'lock', - content: { - icon: IconLock, // 锁定 icon - title: intlNode('lock'), - /* istanbul ignore next */ - action(node: Node) { - node.lock(); - }, - }, - /* istanbul ignore next */ - condition: (node: Node) => { - return engineConfig.get('enableCanvasLock', false) && node.isContainer() && !node.isLocked; - }, - important: true, - }, - { - name: 'unlock', - content: { - icon: IconUnlock, // 解锁 icon - title: intlNode('unlock'), - /* istanbul ignore next */ - action(node: Node) { - node.lock(false); - }, - }, - /* istanbul ignore next */ - condition: (node: Node) => { - return engineConfig.get('enableCanvasLock', false) && node.isContainer() && node.isLocked; - }, - important: true, - }, -]; - -export function removeBuiltinComponentAction(name: string) { - const i = builtinComponentActions.findIndex((action) => action.name === name); - if (i > -1) { - builtinComponentActions.splice(i, 1); - } -} -export function addBuiltinComponentAction(action: ComponentAction) { - builtinComponentActions.push(action); -} - -export function modifyBuiltinComponentAction( - actionName: string, - handle: (action: ComponentAction) => void, -) { - const builtinAction = builtinComponentActions.find((action) => action.name === actionName); - if (builtinAction) { - handle(builtinAction); - } -} - -registerMetadataTransducer(legacyIssues, 2, 'legacy-issues'); // should use a high level priority, eg: 2 -registerMetadataTransducer(componentDefaults, 100, 'component-defaults'); diff --git a/packages/designer/src/context-menu-actions.scss b/packages/designer/src/context-menu-actions.scss new file mode 100644 index 0000000000..863c929447 --- /dev/null +++ b/packages/designer/src/context-menu-actions.scss @@ -0,0 +1,10 @@ +.engine-context-menu { + &.next-menu.next-ver .next-menu-item { + padding-right: 30px; + + .next-menu-item-inner { + height: var(--context-menu-item-height, 30px); + line-height: var(--context-menu-item-height, 30px); + } + } +} \ No newline at end of file diff --git a/packages/designer/src/context-menu-actions.ts b/packages/designer/src/context-menu-actions.ts new file mode 100644 index 0000000000..c88e03ac65 --- /dev/null +++ b/packages/designer/src/context-menu-actions.ts @@ -0,0 +1,233 @@ +import { IPublicTypeContextMenuAction, IPublicEnumContextMenuType, IPublicTypeContextMenuItem, IPublicApiMaterial, IPublicModelPluginContext } from '@alilc/lowcode-types'; +import { IDesigner, INode } from './designer'; +import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties, uniqueId } from '@alilc/lowcode-utils'; +import { Menu } from '@alifd/next'; +import { engineConfig } from '@alilc/lowcode-editor-core'; +import './context-menu-actions.scss'; + +export interface IContextMenuActions { + actions: IPublicTypeContextMenuAction[]; + + adjustMenuLayoutFn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]; + + addMenuAction: IPublicApiMaterial['addContextMenuOption']; + + removeMenuAction: IPublicApiMaterial['removeContextMenuOption']; + + adjustMenuLayout: IPublicApiMaterial['adjustContextMenuLayout']; +} + +let adjustMenuLayoutFn: Function = (actions: IPublicTypeContextMenuAction[]) => actions; + +export class GlobalContextMenuActions { + enableContextMenu: boolean; + + dispose: Function[]; + + contextMenuActionsMap: Map<string, ContextMenuActions> = new Map(); + + constructor() { + this.dispose = []; + + engineConfig.onGot('enableContextMenu', (enable) => { + if (this.enableContextMenu === enable) { + return; + } + this.enableContextMenu = enable; + this.dispose.forEach(d => d()); + if (enable) { + this.initEvent(); + } + }); + } + + handleContextMenu = ( + event: MouseEvent, + ) => { + event.stopPropagation(); + event.preventDefault(); + + const actions: IPublicTypeContextMenuAction[] = []; + let contextMenu: ContextMenuActions = this.contextMenuActionsMap.values().next().value; + this.contextMenuActionsMap.forEach((contextMenu) => { + actions.push(...contextMenu.actions); + }); + + let destroyFn: Function | undefined; + + const destroy = () => { + destroyFn?.(); + }; + const pluginContext: IPublicModelPluginContext = contextMenu.designer.editor.get('pluginContext') as IPublicModelPluginContext; + + const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, { + nodes: [], + destroy, + event, + pluginContext, + }); + + if (!menus.length) { + return; + } + + const layoutMenu = adjustMenuLayoutFn(menus); + + const menuNode = parseContextMenuAsReactNode(layoutMenu, { + destroy, + nodes: [], + pluginContext, + }); + + const target = event.target; + + const { top, left } = target?.getBoundingClientRect(); + + const menuInstance = Menu.create({ + target: event.target, + offset: [event.clientX - left, event.clientY - top], + children: menuNode, + className: 'engine-context-menu', + }); + + destroyFn = (menuInstance as any).destroy; + }; + + initEvent() { + this.dispose.push( + (() => { + const handleContextMenu = (e: MouseEvent) => { + this.handleContextMenu(e); + }; + + document.addEventListener('contextmenu', handleContextMenu); + + return () => { + document.removeEventListener('contextmenu', handleContextMenu); + }; + })(), + ); + } + + registerContextMenuActions(contextMenu: ContextMenuActions) { + this.contextMenuActionsMap.set(contextMenu.id, contextMenu); + } +} + +const globalContextMenuActions = new GlobalContextMenuActions(); + +export class ContextMenuActions implements IContextMenuActions { + actions: IPublicTypeContextMenuAction[] = []; + + designer: IDesigner; + + dispose: Function[]; + + enableContextMenu: boolean; + + id: string = uniqueId('contextMenu');; + + constructor(designer: IDesigner) { + this.designer = designer; + this.dispose = []; + + engineConfig.onGot('enableContextMenu', (enable) => { + if (this.enableContextMenu === enable) { + return; + } + this.enableContextMenu = enable; + this.dispose.forEach(d => d()); + if (enable) { + this.initEvent(); + } + }); + + globalContextMenuActions.registerContextMenuActions(this); + } + + handleContextMenu = ( + nodes: INode[], + event: MouseEvent, + ) => { + const designer = this.designer; + event.stopPropagation(); + event.preventDefault(); + + const actions = designer.contextMenuActions.actions; + + const { bounds } = designer.project.simulator?.viewport || { bounds: { left: 0, top: 0 } }; + const { left: simulatorLeft, top: simulatorTop } = bounds; + + let destroyFn: Function | undefined; + + const destroy = () => { + destroyFn?.(); + }; + + const pluginContext: IPublicModelPluginContext = this.designer.editor.get('pluginContext') as IPublicModelPluginContext; + + const menus: IPublicTypeContextMenuItem[] = parseContextMenuProperties(actions, { + nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!), + destroy, + event, + pluginContext, + }); + + if (!menus.length) { + return; + } + + const layoutMenu = adjustMenuLayoutFn(menus); + + const menuNode = parseContextMenuAsReactNode(layoutMenu, { + destroy, + nodes: nodes.map(d => designer.shellModelFactory.createNode(d)!), + pluginContext, + }); + + destroyFn = createContextMenu(menuNode, { + event, + offset: [simulatorLeft, simulatorTop], + }); + }; + + initEvent() { + const designer = this.designer; + this.dispose.push( + designer.editor.eventBus.on('designer.builtinSimulator.contextmenu', ({ + node, + originalEvent, + }: { + node: INode; + originalEvent: MouseEvent; + }) => { + originalEvent.stopPropagation(); + originalEvent.preventDefault(); + // 如果右键的节点不在 当前选中的节点中,选中该节点 + if (!designer.currentSelection.has(node.id)) { + designer.currentSelection.select(node.id); + } + const nodes = designer.currentSelection.getNodes(); + this.handleContextMenu(nodes, originalEvent); + }), + ); + } + + addMenuAction(action: IPublicTypeContextMenuAction) { + this.actions.push({ + type: IPublicEnumContextMenuType.MENU_ITEM, + ...action, + }); + } + + removeMenuAction(name: string) { + const i = this.actions.findIndex((action) => action.name === name); + if (i > -1) { + this.actions.splice(i, 1); + } + } + + adjustMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) { + adjustMenuLayoutFn = fn; + } +} \ No newline at end of file diff --git a/packages/designer/src/designer/active-tracker.ts b/packages/designer/src/designer/active-tracker.ts index c433970b84..74d865673f 100644 --- a/packages/designer/src/designer/active-tracker.ts +++ b/packages/designer/src/designer/active-tracker.ts @@ -1,34 +1,43 @@ -import { EventEmitter } from 'events'; -import { LocationDetail } from './location'; -import { Node, isNode } from '../document/node/node'; -import { ComponentInstance } from '../simulator'; -import { obx } from '@alilc/lowcode-editor-core'; - -export interface ActiveTarget { - node: Node; - detail?: LocationDetail; - instance?: ComponentInstance; +import { INode } from '../document/node/node'; +import { obx, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { + IPublicTypeActiveTarget, + IPublicModelActiveTracker, +} from '@alilc/lowcode-types'; +import { isNode } from '@alilc/lowcode-utils'; + +export interface IActiveTracker extends Omit< IPublicModelActiveTracker, 'track' | 'onChange' > { + _target: ActiveTarget | INode; + + track(originalTarget: ActiveTarget | INode): void; + + onChange(fn: (target: ActiveTarget) => void): () => void; +} + +export interface ActiveTarget extends Omit< IPublicTypeActiveTarget, 'node' > { + node: INode; } -export class ActiveTracker { - private emitter = new EventEmitter(); +export class ActiveTracker implements IActiveTracker { + @obx.ref private _target?: ActiveTarget | INode; - @obx.ref private _target?: ActiveTarget; + private emitter: IEventBus = createModuleEventBus('ActiveTracker'); - track(target: ActiveTarget | Node) { - if (isNode(target)) { - target = { node: target }; + track(originalTarget: ActiveTarget | INode) { + let target = originalTarget; + if (isNode(originalTarget)) { + target = { node: originalTarget as INode }; } this._target = target; this.emitter.emit('change', target); } get currentNode() { - return this._target?.node; + return (this._target as ActiveTarget)?.node; } get detail() { - return this._target?.detail; + return (this._target as ActiveTarget)?.detail; } /** @@ -40,7 +49,7 @@ export class ActiveTracker { } get instance() { - return this._target?.instance; + return (this._target as ActiveTarget)?.instance; } onChange(fn: (target: ActiveTarget) => void): () => void { diff --git a/packages/designer/src/designer/builtin-hotkey.ts b/packages/designer/src/designer/builtin-hotkey.ts deleted file mode 100644 index 7a226f4f11..0000000000 --- a/packages/designer/src/designer/builtin-hotkey.ts +++ /dev/null @@ -1,354 +0,0 @@ -import { hotkey, Editor, globalContext } from '@alilc/lowcode-editor-core'; -import { isFormEvent } from '@alilc/lowcode-utils'; -import { focusing } from './focusing'; -import { insertChildren, TransformStage } from '../document'; -import clipboard from './clipboard'; - -export function isInLiveEditing() { - if (globalContext.has(Editor)) { - return Boolean( - globalContext.get(Editor).get('designer')?.project?.simulator?.liveEditing?.editing, - ); - } -} - -/* istanbul ignore next */ -function getNextForSelect(next: any, head?: any, parent?: any): any { - if (next) { - if (!head) { - return next; - } - - let ret; - if (next.isContainer()) { - const children = next.getChildren() || []; - if (children && !children.isEmpty()) { - ret = getNextForSelect(children.get(0)); - if (ret) { - return ret; - } - } - } - - ret = getNextForSelect(next.nextSibling); - if (ret) { - return ret; - } - } - - if (parent) { - return getNextForSelect(parent.nextSibling, false, parent.getParent()); - } - - return null; -} - -/* istanbul ignore next */ -function getPrevForSelect(prev: any, head?: any, parent?: any): any { - if (prev) { - let ret; - if (!head && prev.isContainer()) { - const children = prev.getChildren() || []; - const lastChild = children && !children.isEmpty() ? children.get(children.size - 1) : null; - - ret = getPrevForSelect(lastChild); - if (ret) { - return ret; - } - } - - if (!head) { - return prev; - } - - ret = getPrevForSelect(prev.prevSibling); - if (ret) { - return ret; - } - } - - if (parent) { - return parent; - } - - return null; -} - -// hotkey binding -hotkey.bind(['backspace', 'del'], (e: KeyboardEvent) => { - if (isInLiveEditing()) return; - // TODO: use focus-tracker - const doc = focusing.focusDesigner?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - - const sel = doc.selection; - const topItems = sel.getTopNodes(); - // TODO: check can remove - topItems.forEach((node) => { - if (node.canPerformAction('remove')) { - doc.removeNode(node); - } - }); - sel.clear(); -}); - -hotkey.bind('escape', (e: KeyboardEvent) => { - // const currentFocus = focusing.current; - if (isInLiveEditing()) return; - const sel = focusing.focusDesigner?.currentDocument?.selection; - if (isFormEvent(e) || !sel) { - return; - } - e.preventDefault(); - - sel.clear(); - // currentFocus.esc(); -}); - -// command + c copy command + x cut -hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => { - if (isInLiveEditing()) return; - const doc = focusing.focusDesigner?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - - let selected = doc.selection.getTopNodes(true); - selected = selected.filter((node) => { - return node.canPerformAction('copy'); - }); - if (!selected || selected.length < 1) { - return; - } - - const componentsMap = {}; - const componentsTree = selected.map((item) => item.export(TransformStage.Clone)); - - // FIXME: clear node.id - - const data = { type: 'nodeSchema', componentsMap, componentsTree }; - - clipboard.setData(data); - - const cutMode = action && action.indexOf('x') > 0; - if (cutMode) { - selected.forEach((node) => { - const parentNode = node.getParent(); - parentNode?.select(); - node.remove(); - }); - } -}); - -// command + v paste -hotkey.bind(['command+v', 'ctrl+v'], (e) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !designer || !doc) { - return; - } - /* istanbul ignore next */ - clipboard.waitPasteData(e, ({ componentsTree }) => { - if (componentsTree) { - const { target, index } = designer.getSuitableInsertion(componentsTree) || {}; - if (!target) { - return; - } - let canAddComponentsTree = componentsTree.filter((i) => { - return doc.checkNestingUp(target, i); - }); - if (canAddComponentsTree.length === 0) return; - const nodes = insertChildren(target, canAddComponentsTree, index); - if (nodes) { - doc.selection.selectAll(nodes.map((o) => o.id)); - setTimeout(() => designer.activeTracker.track(nodes[0]), 10); - } - } - }); -}); - -// command + z undo -hotkey.bind(['command+z', 'ctrl+z'], (e) => { - if (isInLiveEditing()) return; - const his = focusing.focusDesigner?.currentHistory; - if (isFormEvent(e) || !his) { - return; - } - - e.preventDefault(); - const selection = focusing.focusDesigner?.currentSelection; - const curSelected = Array.from(selection?.selected); - his.back(); - selection?.selectAll(curSelected); -}); - -// command + shift + z redo -hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e) => { - if (isInLiveEditing()) return; - const his = focusing.focusDesigner?.currentHistory; - if (isFormEvent(e) || !his) { - return; - } - e.preventDefault(); - const selection = focusing.focusDesigner?.currentSelection; - const curSelected = Array.from(selection?.selected); - his.forward(); - selection?.selectAll(curSelected); -}); - -// sibling selection -hotkey.bind(['left', 'right'], (e, action) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - const firstNode = selected[0]; - const silbing = action === 'left' ? firstNode?.prevSibling : firstNode?.nextSibling; - silbing?.select(); -}); - -hotkey.bind(['up', 'down'], (e, action) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - const firstNode = selected[0]; - - if (action === 'down') { - const next = getNextForSelect(firstNode, true, firstNode.getParent()); - next?.select(); - } else if (action === 'up') { - const prev = getPrevForSelect(firstNode, true, firstNode.getParent()); - prev?.select(); - } -}); - -hotkey.bind(['option+left', 'option+right'], (e, action) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode.getParent(); - if (!parent) return; - - const isPrev = action && /(left)$/.test(action); - - const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling; - if (silbing) { - if (isPrev) { - parent.insertBefore(firstNode, silbing); - } else { - parent.insertAfter(firstNode, silbing); - } - firstNode?.select(); - } -}); - -hotkey.bind(['option+up'], (e) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode.getParent(); - if (!parent) { - return; - } - - const silbing = firstNode.prevSibling; - if (silbing) { - if (silbing.isContainer()) { - const place = silbing.getSuitablePlace(firstNode, null); - place.container.insertAfter(firstNode, place.ref); - } else { - parent.insertBefore(firstNode, silbing); - } - firstNode?.select(); - } else { - const place = parent.getSuitablePlace(firstNode, null); // upwards - if (place) { - place.container.insertBefore(firstNode, place.ref); - firstNode?.select(); - } - } -}); - -hotkey.bind(['option+down'], (e) => { - if (isInLiveEditing()) return; - const designer = focusing.focusDesigner; - const doc = designer?.currentDocument; - if (isFormEvent(e) || !doc) { - return; - } - e.preventDefault(); - const selected = doc.selection.getTopNodes(true); - if (!selected || selected.length < 1) { - return; - } - // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 - // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 - - const firstNode = selected[0]; - const parent = firstNode.getParent(); - if (!parent) { - return; - } - - const silbing = firstNode.nextSibling; - if (silbing) { - if (silbing.isContainer()) { - // const place = silbing.getSuitablePlace(firstNode, null); - silbing.insertBefore(firstNode, undefined); - // place.container.insertBefore(firstNode, place.ref); - } else { - parent.insertAfter(firstNode, silbing); - } - firstNode?.select(); - } else { - const place = parent.getSuitablePlace(firstNode, null); // upwards - if (place) { - place.container.insertAfter(firstNode, place.ref); - firstNode?.select(); - } - } -}); diff --git a/packages/designer/src/designer/clipboard.ts b/packages/designer/src/designer/clipboard.ts index dcaa21c0ff..34ce2b5b53 100644 --- a/packages/designer/src/designer/clipboard.ts +++ b/packages/designer/src/designer/clipboard.ts @@ -1,3 +1,5 @@ +import { IPublicModelClipboard } from '@alilc/lowcode-types'; + function getDataFromPasteEvent(event: ClipboardEvent) { const { clipboardData } = event; if (!clipboardData) { @@ -18,31 +20,26 @@ function getDataFromPasteEvent(event: ClipboardEvent) { }; } } catch (error) { - /* - const html = clipboardData.getData('text/html'); - if (html !== '') { - // TODO: clear the html - return { - code: '<div dangerouslySetInnerHTML={ __html: html } />', - maps: {}, - }; - } - */ // TODO: open the parser implement return { }; - /* - return { - code: clipboardData.getData('text/plain'), - maps: {}, - }; */ } } -class Clipboard { +export interface IClipboard extends IPublicModelClipboard { + + initCopyPaster(el: HTMLTextAreaElement): void; + + injectCopyPaster(document: Document): void; +} +class Clipboard implements IClipboard { private copyPasters: HTMLTextAreaElement[] = []; private waitFn?: (data: any, e: ClipboardEvent) => void; + constructor() { + this.injectCopyPaster(document); + } + isCopyPasteEvent(e: Event) { this.isCopyPaster(e.target); } @@ -71,12 +68,18 @@ class Clipboard { } injectCopyPaster(document: Document) { - if (this.copyPasters.find(x => x.ownerDocument === document)) { + if (this.copyPasters.find((x) => x.ownerDocument === document)) { return; } const copyPaster = document.createElement<'textarea'>('textarea'); copyPaster.style.cssText = 'position: absolute;left: -9999px;top:-100px'; - document.body.appendChild(copyPaster); + if (document.body) { + document.body.appendChild(copyPaster); + } else { + document.addEventListener('DOMContentLoaded', () => { + document.body.appendChild(copyPaster); + }); + } const dispose = this.initCopyPaster(copyPaster); return () => { dispose(); @@ -84,8 +87,8 @@ class Clipboard { }; } - setData(data: any) { - const copyPaster = this.copyPasters.find(x => x.ownerDocument); + setData(data: any): void { + const copyPaster = this.copyPasters.find((x) => x.ownerDocument); if (!copyPaster) { return; } @@ -96,12 +99,12 @@ class Clipboard { copyPaster.blur(); } - waitPasteData(e: KeyboardEvent, cb: (data: any, e: ClipboardEvent) => void) { - const win = e.view; + waitPasteData(keyboardEvent: KeyboardEvent, cb: (data: any, e: ClipboardEvent) => void) { + const win = keyboardEvent.view; if (!win) { return; } - const copyPaster = this.copyPasters.find(cp => cp.ownerDocument === win.document); + const copyPaster = this.copyPasters.find((cp) => cp.ownerDocument === win.document); if (copyPaster) { copyPaster.select(); this.waitFn = cb; @@ -109,4 +112,4 @@ class Clipboard { } } -export default new Clipboard(); +export const clipboard = new Clipboard(); diff --git a/packages/designer/src/designer/designer-view.tsx b/packages/designer/src/designer/designer-view.tsx index 6ce25f6065..aaf0c9583e 100644 --- a/packages/designer/src/designer/designer-view.tsx +++ b/packages/designer/src/designer/designer-view.tsx @@ -4,16 +4,19 @@ import BuiltinDragGhostComponent from './drag-ghost'; import { Designer, DesignerProps } from './designer'; import { ProjectView } from '../project'; import './designer.less'; -import clipboard from './clipboard'; -export class DesignerView extends Component<DesignerProps & { +type IProps = DesignerProps & { designer?: Designer; -}> { +}; + +export class DesignerView extends Component<IProps> { readonly designer: Designer; + readonly viewName: string | undefined; - constructor(props: any) { + constructor(props: IProps) { super(props); const { designer, ...designerProps } = props; + this.viewName = designer?.viewName; if (designer) { this.designer = designer; designer.setProps(designerProps); @@ -40,7 +43,6 @@ export class DesignerView extends Component<DesignerProps & { if (onMount) { onMount(this.designer); } - clipboard.injectCopyPaster(document); this.designer.postEvent('mount', this.designer); } diff --git a/packages/designer/src/designer/designer.ts b/packages/designer/src/designer/designer.ts index 9663574dbe..1dd4bc04e6 100644 --- a/packages/designer/src/designer/designer.ts +++ b/packages/designer/src/designer/designer.ts @@ -1,63 +1,166 @@ import { ComponentType } from 'react'; import { obx, computed, autorun, makeObservable, IReactionPublic, IReactionOptions, IReactionDisposer } from '@alilc/lowcode-editor-core'; import { - ProjectSchema, - ComponentMetadata, - ComponentAction, - NpmInfo, - IEditor, - CompositeObject, - PropsList, - isNodeSchema, - NodeSchema, + IPublicTypeProjectSchema, + IPublicTypeComponentMetadata, + IPublicTypeComponentAction, + IPublicTypeNpmInfo, + IPublicModelEditor, + IPublicTypeCompositeObject, + IPublicTypePropsList, + IPublicTypeNodeSchema, + IPublicTypePropsTransducer, + IShellModelFactory, + IPublicModelDragObject, + IPublicTypeScrollable, + IPublicModelScroller, + IPublicTypeLocationData, + IPublicEnumTransformStage, + IPublicModelLocateEvent, } from '@alilc/lowcode-types'; -import { megreAssets, AssetsJson } from '@alilc/lowcode-utils'; -import { Project } from '../project'; -import { Node, DocumentModel, insertChildren, ParentalNode, TransformStage } from '../document'; -import { ComponentMeta } from '../component-meta'; +import { mergeAssets, IPublicTypeAssetsJson, isNodeSchema, isDragNodeObject, isDragNodeDataObject, isLocationChildrenDetail, Logger } from '@alilc/lowcode-utils'; +import { IProject, Project } from '../project'; +import { Node, DocumentModel, insertChildren, INode, ISelection } from '../document'; +import { ComponentMeta, IComponentMeta } from '../component-meta'; import { INodeSelector, Component } from '../simulator'; -import { Scroller, IScrollable } from './scroller'; -import { Dragon, isDragNodeObject, isDragNodeDataObject, LocateEvent, DragObject } from './dragon'; -import { ActiveTracker } from './active-tracker'; +import { Scroller } from './scroller'; +import { Dragon, IDragon } from './dragon'; +import { ActiveTracker, IActiveTracker } from './active-tracker'; import { Detecting } from './detecting'; -import { DropLocation, LocationData, isLocationChildrenDetail } from './location'; +import { DropLocation } from './location'; import { OffsetObserver, createOffsetObserver } from './offset-observer'; -import { focusing } from './focusing'; -import { SettingTopEntry } from './setting'; +import { ISettingTopEntry, SettingTopEntry } from './setting'; import { BemToolsManager } from '../builtin-simulator/bem-tools/manager'; +import { ComponentActions } from '../component-actions'; +import { ContextMenuActions, IContextMenuActions } from '../context-menu-actions'; + +const logger = new Logger({ level: 'warn', bizName: 'designer' }); export interface DesignerProps { - editor: IEditor; + [key: string]: any; + editor: IPublicModelEditor; + shellModelFactory: IShellModelFactory; className?: string; style?: object; - defaultSchema?: ProjectSchema; + defaultSchema?: IPublicTypeProjectSchema; hotkeys?: object; - simulatorProps?: object | ((document: DocumentModel) => object); + viewName?: string; + simulatorProps?: Record<string, any> | ((document: DocumentModel) => object); simulatorComponent?: ComponentType<any>; dragGhostComponent?: ComponentType<any>; suspensed?: boolean; - componentMetadatas?: ComponentMetadata[]; - globalComponentActions?: ComponentAction[]; + componentMetadatas?: IPublicTypeComponentMetadata[]; + globalComponentActions?: IPublicTypeComponentAction[]; onMount?: (designer: Designer) => void; - onDragstart?: (e: LocateEvent) => void; - onDrag?: (e: LocateEvent) => void; - onDragend?: (e: { dragObject: DragObject; copy: boolean }, loc?: DropLocation) => void; - [key: string]: any; + onDragstart?: (e: IPublicModelLocateEvent) => void; + onDrag?: (e: IPublicModelLocateEvent) => void; + onDragend?: ( + e: { dragObject: IPublicModelDragObject; copy: boolean }, + loc?: DropLocation, + ) => void; } -export class Designer { - readonly dragon = new Dragon(this); +export interface IDesigner { + readonly shellModelFactory: IShellModelFactory; + + viewName: string | undefined; + + readonly project: IProject; + + get dragon(): IDragon; + + get activeTracker(): IActiveTracker; + + get componentActions(): ComponentActions; + + get contextMenuActions(): ContextMenuActions; + + get editor(): IPublicModelEditor; + + get detecting(): Detecting; + + get simulatorComponent(): ComponentType<any> | undefined; + + get currentSelection(): ISelection; + + createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller; + + refreshComponentMetasMap(): void; + + createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null; + + /** + * 创建插入位置,考虑放到 dragon 中 + */ + createLocation(locationData: IPublicTypeLocationData<INode>): DropLocation; + + get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component }; + + loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise<void>; + + getComponentMeta( + componentName: string, + generateMetadata?: () => IPublicTypeComponentMetadata | null, + ): IComponentMeta; + + clearLocation(): void; + + createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null; + + getComponentMetasMap(): Map<string, IComponentMeta>; + + addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage): void; + + postEvent(event: string, ...args: any[]): void; + + transformProps(props: IPublicTypeCompositeObject | IPublicTypePropsList, node: Node, stage: IPublicEnumTransformStage): IPublicTypeCompositeObject | IPublicTypePropsList; + + createSettingEntry(nodes: INode[]): ISettingTopEntry; + + autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions<any, any>): IReactionDisposer; +} + +export class Designer implements IDesigner { + dragon: IDragon; + + viewName: string | undefined; + + readonly componentActions = new ComponentActions(); + + readonly contextMenuActions: IContextMenuActions; readonly activeTracker = new ActiveTracker(); readonly detecting = new Detecting(); - readonly project: Project; + readonly project: IProject; - readonly editor: IEditor; + readonly editor: IPublicModelEditor; readonly bemToolsManager = new BemToolsManager(this); + readonly shellModelFactory: IShellModelFactory; + + private _dropLocation?: DropLocation; + + private propsReducers = new Map<IPublicEnumTransformStage, IPublicTypePropsTransducer[]>(); + + private _lostComponentMetasMap = new Map<string, ComponentMeta>(); + + private props?: DesignerProps; + + private oobxList: OffsetObserver[] = []; + + private selectionDispose: undefined | (() => void); + + @obx.ref private _componentMetasMap = new Map<string, IComponentMeta>(); + + @obx.ref private _simulatorComponent?: ComponentType<any>; + + @obx.ref private _simulatorProps?: Record<string, any> | ((project: IProject) => object); + + @obx.ref private _suspensed = false; + get currentDocument() { return this.project.currentDocument; } @@ -72,25 +175,19 @@ export class Designer { constructor(props: DesignerProps) { makeObservable(this); - const { editor } = props; + const { editor, viewName, shellModelFactory } = props; this.editor = editor; + this.viewName = viewName; + this.shellModelFactory = shellModelFactory; this.setProps(props); - this.project = new Project(this, props.defaultSchema); + this.project = new Project(this, props.defaultSchema, viewName); - let startTime: any; - let src = ''; + this.dragon = new Dragon(this); this.dragon.onDragstart((e) => { - startTime = Date.now() / 1000; this.detecting.enable = false; const { dragObject } = e; if (isDragNodeObject(dragObject)) { - const node = dragObject.nodes[0]?.parent; - const npm = node?.componentMeta?.npm; - src = - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - node?.componentMeta?.componentName || - ''; if (dragObject.nodes.length === 1) { if (dragObject.nodes[0].parent) { // ensure current selecting @@ -108,6 +205,8 @@ export class Designer { this.postEvent('dragstart', e); }); + this.contextMenuActions = new ContextMenuActions(this); + this.dragon.onDrag((e) => { if (this.props?.onDrag) { this.props.onDrag(e); @@ -117,10 +216,11 @@ export class Designer { this.dragon.onDragend((e) => { const { dragObject, copy } = e; + logger.debug('onDragend: dragObject ', dragObject, ' copy ', copy); const loc = this._dropLocation; if (loc) { if (isLocationChildrenDetail(loc.detail) && loc.detail.valid !== false) { - let nodes: Node[] | undefined; + let nodes: INode[] | undefined; if (isDragNodeObject(dragObject)) { nodes = insertChildren(loc.target, [...dragObject.nodes], loc.detail.index, copy); } else if (isDragNodeDataObject(dragObject)) { @@ -133,36 +233,8 @@ export class Designer { nodes = insertChildren(loc.target, nodeData, loc.detail.index); } if (nodes) { - loc.document.selection.selectAll(nodes.map((o) => o.id)); + loc.document?.selection.selectAll(nodes.map((o) => o.id)); setTimeout(() => this.activeTracker.track(nodes![0]), 10); - const endTime: any = Date.now() / 1000; - const parent = nodes[0]?.parent; - const npm = parent?.componentMeta?.npm; - const dest = - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - parent?.componentMeta?.componentName || - ''; - // eslint-disable-next-line no-unused-expressions - // this.postEvent('drag', { - // time: (endTime - startTime).toFixed(2), - // selected: nodes - // ?.map((n) => { - // if (!n) { - // return; - // } - // // eslint-disable-next-line no-shadow - // const npm = n?.componentMeta?.npm; - // return ( - // [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || - // n?.componentMeta?.componentName - // ); - // }) - // .join('&'), - // align: loc?.detail?.near?.align || '', - // pos: loc?.detail?.near?.pos || '', - // src, - // dest, - // }); } } } @@ -174,7 +246,7 @@ export class Designer { }); this.activeTracker.onChange(({ node, detail }) => { - node.document.simulator?.scrollToNode(node, detail); + node.document?.simulator?.scrollToNode(node, detail); }); let historyDispose: undefined | (() => void); @@ -201,16 +273,12 @@ export class Designer { this.postEvent('init', this); this.setupSelection(); setupHistory(); - - // TODO: 先简单实现,后期通过焦点赋值 - focusing.focusDesigner = this; } setupSelection = () => { - let selectionDispose: undefined | (() => void); - if (selectionDispose) { - selectionDispose(); - selectionDispose = undefined; + if (this.selectionDispose) { + this.selectionDispose(); + this.selectionDispose = undefined; } const { currentSelection } = this; // TODO: 避免选中 Page 组件,默认选中第一个子节点;新增规则 或 判断 Live 模式 @@ -219,25 +287,23 @@ export class Designer { currentSelection.selected.length === 0 && this.simulatorProps?.designMode === 'live' ) { - const rootNodeChildrens = this.currentDocument.getRoot().getChildren().children; - if (rootNodeChildrens.length > 0) { + const rootNodeChildrens = this.currentDocument?.getRoot()?.getChildren()?.children; + if (rootNodeChildrens && rootNodeChildrens.length > 0) { currentSelection.select(rootNodeChildrens[0].id); } } this.postEvent('selection.change', currentSelection); if (currentSelection) { - selectionDispose = currentSelection.onSelectionChange(() => { + this.selectionDispose = currentSelection.onSelectionChange(() => { this.postEvent('selection.change', currentSelection); }); } }; postEvent(event: string, ...args: any[]) { - this.editor.emit(`designer.${event}`, ...args); + this.editor.eventBus.emit(`designer.${event}`, ...args); } - private _dropLocation?: DropLocation; - get dropLocation() { return this._dropLocation; } @@ -245,14 +311,16 @@ export class Designer { /** * 创建插入位置,考虑放到 dragon 中 */ - createLocation(locationData: LocationData): DropLocation { + createLocation(locationData: IPublicTypeLocationData<INode>): DropLocation { const loc = new DropLocation(locationData); - if (this._dropLocation && this._dropLocation.document !== loc.document) { - this._dropLocation.document.internalSetDropLocation(null); + if (this._dropLocation && this._dropLocation.document && this._dropLocation.document !== loc.document) { + this._dropLocation.document.dropLocation = null; } this._dropLocation = loc; this.postEvent('dropLocation.change', loc); - loc.document.internalSetDropLocation(loc); + if (loc.document) { + loc.document.dropLocation = loc; + } this.activeTracker.track({ node: loc.target, detail: loc.detail }); return loc; } @@ -261,19 +329,17 @@ export class Designer { * 清除插入位置 */ clearLocation() { - if (this._dropLocation) { - this._dropLocation.document.internalSetDropLocation(null); + if (this._dropLocation && this._dropLocation.document) { + this._dropLocation.document.dropLocation = null; } this.postEvent('dropLocation.change', undefined); this._dropLocation = undefined; } - createScroller(scrollable: IScrollable) { + createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller { return new Scroller(scrollable); } - private oobxList: OffsetObserver[] = []; - createOffsetObserver(nodeInstance: INodeSelector): OffsetObserver | null { const oobx = createOffsetObserver(nodeInstance); this.clearOobxList(); @@ -299,16 +365,17 @@ export class Designer { this.oobxList.forEach((item) => item.compute()); } - createSettingEntry(nodes: Node[]) { + createSettingEntry(nodes: INode[]): ISettingTopEntry { return new SettingTopEntry(this.editor, nodes); } /** * 获得合适的插入位置 + * @deprecated */ getSuitableInsertion( - insertNode?: Node | NodeSchema | NodeSchema[], - ): { target: ParentalNode; index?: number } | null { + insertNode?: INode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[], + ): { target: INode; index?: number } | null { const activeDoc = this.project.currentDocument; if (!activeDoc) { return null; @@ -319,12 +386,12 @@ export class Designer { this.getComponentMeta(insertNode[0].componentName).isModal ) { return { - target: activeDoc.rootNode as ParentalNode, + target: activeDoc.rootNode as INode, }; } const focusNode = activeDoc.focusNode!; const nodes = activeDoc.selection.getNodes(); - const refNode = nodes.find(item => focusNode.contains(item)); + const refNode = nodes.find((item) => focusNode.contains(item)); let target; let index: number | undefined; if (!refNode || refNode === focusNode) { @@ -334,7 +401,7 @@ export class Designer { } else { // FIXME!!, parent maybe null target = refNode.parent!; - index = refNode.index + 1; + index = (refNode.index || 0) + 1; } if (target && insertNode && !target.componentMeta.checkNestingDown(target, insertNode)) { @@ -344,8 +411,6 @@ export class Designer { return { target, index }; } - private props?: DesignerProps; - setProps(nextProps: DesignerProps) { const props = this.props ? { ...this.props, ...nextProps } : nextProps; if (this.props) { @@ -392,24 +457,24 @@ export class Designer { this.props = props; } - async loadIncrementalAssets(incrementalAssets: AssetsJson): Promise<void> { + async loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): Promise<void> { const { components, packages } = incrementalAssets; components && this.buildComponentMetasMap(components); if (packages) { - await this.project.simulator!.setupComponents(packages); + await this.project.simulator?.setupComponents(packages); } if (components) { - // 合并assets - let assets = this.editor.get('assets'); - let newAssets = megreAssets(assets, incrementalAssets); + // 合并 assets + let assets = this.editor.get('assets') || {}; + let newAssets = mergeAssets(assets, incrementalAssets); // 对于 assets 存在需要二次网络下载的过程,必须 await 等待结束之后,再进行事件触发 await this.editor.set('assets', newAssets); } // TODO: 因为涉及修改 prototype.view,之后在 renderer 里修改了 vc 的 view 获取逻辑后,可删除 this.refreshComponentMetasMap(); // 完成加载增量资源后发送事件,方便插件监听并处理相关逻辑 - this.editor.emit('designer.incrementalAssetsReady'); + this.editor.eventBus.emit('designer.incrementalAssetsReady'); } /** @@ -423,19 +488,31 @@ export class Designer { return this.props?.[key]; } - @obx.ref private _simulatorComponent?: ComponentType<any>; - @computed get simulatorComponent(): ComponentType<any> | undefined { return this._simulatorComponent; } - @obx.ref private _simulatorProps?: object | ((document: DocumentModel) => object); - - @computed get simulatorProps(): object | ((project: Project) => object) { + @computed get simulatorProps(): Record<string, any> { + if (typeof this._simulatorProps === 'function') { + return this._simulatorProps(this.project); + } return this._simulatorProps || {}; } - @obx.ref private _suspensed = false; + /** + * 提供给模拟器的参数 + */ + @computed get projectSimulatorProps(): any { + return { + ...this.simulatorProps, + project: this.project, + designer: this, + onMount: (simulator: any) => { + this.project.mountSimulator(simulator); + this.editor.set('simulator', simulator); + }, + }; + } get suspensed(): boolean { return this._suspensed; @@ -449,24 +526,23 @@ export class Designer { } } - get schema(): ProjectSchema { + get schema(): IPublicTypeProjectSchema { return this.project.getSchema(); } - setSchema(schema?: ProjectSchema) { + setSchema(schema?: IPublicTypeProjectSchema) { this.project.load(schema); } - @obx.ref private _componentMetasMap = new Map<string, ComponentMeta>(); - - private _lostComponentMetasMap = new Map<string, ComponentMeta>(); - - buildComponentMetasMap(metas: ComponentMetadata[]) { + buildComponentMetasMap(metas: IPublicTypeComponentMetadata[]) { metas.forEach((data) => this.createComponentMeta(data)); } - createComponentMeta(data: ComponentMetadata): ComponentMeta { + createComponentMeta(data: IPublicTypeComponentMetadata): IComponentMeta | null { const key = data.componentName; + if (!key) { + return null; + } let meta = this._componentMetasMap.get(key); if (meta) { meta.setMetadata(data); @@ -487,14 +563,14 @@ export class Designer { return meta; } - getGlobalComponentActions(): ComponentAction[] | null { + getGlobalComponentActions(): IPublicTypeComponentAction[] | null { return this.props?.globalComponentActions || null; } getComponentMeta( componentName: string, - generateMetadata?: () => ComponentMetadata | null, - ): ComponentMeta { + generateMetadata?: () => IPublicTypeComponentMetadata | null, + ): IComponentMeta { if (this._componentMetasMap.has(componentName)) { return this._componentMetasMap.get(componentName)!; } @@ -517,7 +593,7 @@ export class Designer { return this._componentMetasMap; } - @computed get componentsMap(): { [key: string]: NpmInfo | Component } { + @computed get componentsMap(): { [key: string]: IPublicTypeNpmInfo | Component } { const maps: any = {}; const designer = this; designer._componentMetasMap.forEach((config, key) => { @@ -525,7 +601,7 @@ export class Designer { if (metaData.devMode === 'lowCode') { maps[key] = metaData.schema; } else { - const view = metaData.configure.advanced?.view; + const { view } = config.advanced; if (view) { maps[key] = view; } else { @@ -536,9 +612,7 @@ export class Designer { return maps; } - private propsReducers = new Map<TransformStage, PropsReducer[]>(); - - transformProps(props: CompositeObject | PropsList, node: Node, stage: TransformStage) { + transformProps(props: IPublicTypeCompositeObject | IPublicTypePropsList, node: Node, stage: IPublicEnumTransformStage) { if (Array.isArray(props)) { // current not support, make this future return props; @@ -560,7 +634,11 @@ export class Designer { }, props); } - addPropsReducer(reducer: PropsReducer, stage: TransformStage) { + addPropsReducer(reducer: IPublicTypePropsTransducer, stage: IPublicEnumTransformStage) { + if (!reducer) { + logger.error('reducer is not available'); + return; + } const reducers = this.propsReducers.get(stage); if (reducers) { reducers.push(reducer); @@ -569,7 +647,7 @@ export class Designer { } } - autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions): IReactionDisposer { + autorun(effect: (reaction: IReactionPublic) => void, options?: IReactionOptions<any, any>): IReactionDisposer { return autorun(effect, options); } @@ -577,10 +655,3 @@ export class Designer { // TODO: } } - -export type PropsReducerContext = { stage: TransformStage }; -export type PropsReducer = ( - props: CompositeObject, - node: Node, - ctx?: PropsReducerContext, -) => CompositeObject; diff --git a/packages/designer/src/designer/detecting.ts b/packages/designer/src/designer/detecting.ts index 4fc60adf97..a5d898d6e0 100644 --- a/packages/designer/src/designer/detecting.ts +++ b/packages/designer/src/designer/detecting.ts @@ -1,12 +1,30 @@ -import { makeObservable, obx } from '@alilc/lowcode-editor-core'; -import { EventEmitter } from 'events'; -import { Node, DocumentModel } from '../document'; +import { makeObservable, obx, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { IPublicModelDetecting } from '@alilc/lowcode-types'; +import type { IDocumentModel } from '../document/document-model'; +import type { INode } from '../document/node/node'; const DETECTING_CHANGE_EVENT = 'detectingChange'; +export interface IDetecting extends Omit<IPublicModelDetecting<INode>, + 'capture' | + 'release' | + 'leave' +> { + capture(node: INode | null): void; -export class Detecting { + release(node: INode | null): void; + + leave(document: IDocumentModel | undefined): void; + + get current(): INode | null; +} + +export class Detecting implements IDetecting { @obx.ref private _enable = true; + /** + * 控制大纲树 hover 时是否出现悬停效果 + * TODO: 将该逻辑从设计器中抽离出来 + */ get enable() { return this._enable; } @@ -20,9 +38,9 @@ export class Detecting { @obx.ref xRayMode = false; - @obx.ref private _current: Node | null = null; + @obx.ref private _current: INode | null = null; - private emitter: EventEmitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('Detecting'); constructor() { makeObservable(this); @@ -32,27 +50,27 @@ export class Detecting { return this._current; } - capture(node: Node | null) { + capture(node: INode | null) { if (this._current !== node) { this._current = node; this.emitter.emit(DETECTING_CHANGE_EVENT, this.current); } } - release(node: Node | null) { + release(node: INode | null) { if (this._current === node) { this._current = null; this.emitter.emit(DETECTING_CHANGE_EVENT, this.current); } } - leave(document: DocumentModel | undefined) { + leave(document: IDocumentModel | undefined) { if (this.current && this.current.document === document) { this._current = null; } } - onDetectingChange(fn: (node: Node) => void) { + onDetectingChange(fn: (node: INode) => void) { this.emitter.on(DETECTING_CHANGE_EVENT, fn); return () => { this.emitter.off(DETECTING_CHANGE_EVENT, fn); diff --git a/packages/designer/src/designer/drag-ghost/ghost.less b/packages/designer/src/designer/drag-ghost/ghost.less index 7ab61b41d6..1d9c03552f 100644 --- a/packages/designer/src/designer/drag-ghost/ghost.less +++ b/packages/designer/src/designer/drag-ghost/ghost.less @@ -7,8 +7,8 @@ flex-direction: column; align-items: center; pointer-events: none; - background-color: rgba(0, 0, 0, 0.4); - box-shadow: 0 0 6px grey; + background-color: var(--color-block-background-deep-dark, rgba(0, 0, 0, 0.4)); + box-shadow: 0 0 6px var(--color-block-background-shallow, grey); transform: translate(-10%, -50%); .lc-ghost { .lc-ghost-title { diff --git a/packages/designer/src/designer/drag-ghost/index.tsx b/packages/designer/src/designer/drag-ghost/index.tsx index 331ca5b289..50fca2f28e 100644 --- a/packages/designer/src/designer/drag-ghost/index.tsx +++ b/packages/designer/src/designer/drag-ghost/index.tsx @@ -1,10 +1,10 @@ import { Component, ReactElement } from 'react'; import { observer, obx, Title, makeObservable } from '@alilc/lowcode-editor-core'; import { Designer } from '../designer'; -import { DragObject, isDragNodeObject } from '../dragon'; +import { isDragNodeObject } from '../dragon'; import { isSimulatorHost } from '../../simulator'; import './ghost.less'; -import { I18nData, NodeSchema } from '@alilc/lowcode-types'; +import { IPublicTypeI18nData, IPublicTypeNodeSchema, IPublicModelDragObject } from '@alilc/lowcode-types'; type offBinding = () => any; @@ -12,7 +12,7 @@ type offBinding = () => any; export default class DragGhost extends Component<{ designer: Designer }> { private dispose: offBinding[] = []; - @obx.ref private titles: (string | I18nData | ReactElement)[] | null = null; + @obx.ref private titles: (string | IPublicTypeI18nData | ReactElement)[] | null = null; @obx.ref private x = 0; @@ -39,7 +39,7 @@ export default class DragGhost extends Component<{ designer: Designer }> { this.y = e.globalY; if (isSimulatorHost(e.sensor)) { const container = e.sensor.getDropContainer(e); - if (container?.container.componentMeta.getMetadata().configure.advanced?.isAbsoluteLayoutContainer) { + if (container?.container.componentMeta.advanced.isAbsoluteLayoutContainer) { this.isAbsoluteLayoutContainer = true; return; } @@ -54,14 +54,14 @@ export default class DragGhost extends Component<{ designer: Designer }> { ]; } - getTitles(dragObject: DragObject) { + getTitles(dragObject: IPublicModelDragObject) { if (isDragNodeObject(dragObject)) { return dragObject.nodes.map((node) => node.title); } const dataList = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data]; - return dataList.map((item: NodeSchema, i) => (this.props.designer.getComponentMeta(item.componentName).title)); + return dataList.map((item: IPublicTypeNodeSchema, i) => (this.props.designer.getComponentMeta(item.componentName).title)); } componentWillUnmount() { diff --git a/packages/designer/src/designer/dragon.ts b/packages/designer/src/designer/dragon.ts index a74e36b98d..8dcce2b4a2 100644 --- a/packages/designer/src/designer/dragon.ts +++ b/packages/designer/src/designer/dragon.ts @@ -1,126 +1,57 @@ -import { EventEmitter } from 'events'; -import { obx, makeObservable } from '@alilc/lowcode-editor-core'; -import { NodeSchema } from '@alilc/lowcode-types'; +import { obx, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { + IPublicTypeDragNodeObject, + IPublicTypeDragAnyObject, + IPublicEnumDragObjectType, + IPublicTypeDragNodeDataObject, + IPublicModelDragObject, + IPublicModelNode, + IPublicModelDragon, + IPublicModelLocateEvent, + IPublicModelSensor, +} from '@alilc/lowcode-types'; import { setNativeSelection, cursor } from '@alilc/lowcode-utils'; -import { DropLocation } from './location'; -import { Node, DocumentModel } from '../document'; -import { ISimulatorHost, isSimulatorHost, NodeInstance, ComponentInstance } from '../simulator'; -import { Designer } from './designer'; +import { INode, Node } from '../document'; +import { ISimulatorHost, isSimulatorHost } from '../simulator'; +import { IDesigner } from './designer'; import { makeEventsHandler } from '../utils/misc'; -export interface LocateEvent { +export interface ILocateEvent extends IPublicModelLocateEvent { readonly type: 'LocateEvent'; - /** - * 浏览器窗口坐标系 - */ - readonly globalX: number; - readonly globalY: number; - /** - * 原始事件 - */ - readonly originalEvent: MouseEvent | DragEvent; - /** - * 拖拽对象 - */ - readonly dragObject: DragObject; /** * 激活的感应器 */ - sensor?: ISensor; - - // ======= 以下是 激活的 sensor 将填充的值 ======== - /** - * 浏览器事件响应目标 - */ - target?: Element | null; - /** - * 当前激活文档画布坐标系 - */ - canvasX?: number; - canvasY?: number; - /** - * 激活或目标文档 - */ - documentModel?: DocumentModel; - /** - * 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有 - */ - fixed?: true; + sensor?: IPublicModelSensor; } /** - * 拖拽敏感板 + * @deprecated use same function in @alilc/lowcode-utils */ -export interface ISensor { - /** - * 是否可响应,比如面板被隐藏,可设置该值 false - */ - readonly sensorAvailable: boolean; - /** - * 给事件打补丁 - */ - fixEvent(e: LocateEvent): LocateEvent; - /** - * 定位并激活 - */ - locate(e: LocateEvent): DropLocation | undefined | null; - /** - * 是否进入敏感板区域 - */ - isEnter(e: LocateEvent): boolean; - /** - * 取消激活 - */ - deactiveSensor(): void; - /** - * 获取节点实例 - */ - getNodeInstanceFromElement(e: Element | null): NodeInstance<ComponentInstance> | null; -} - -export type DragObject = DragNodeObject | DragNodeDataObject | DragAnyObject; - -export enum DragObjectType { - // eslint-disable-next-line no-shadow - Node = 'node', - NodeData = 'nodedata', +export function isDragNodeObject(obj: any): obj is IPublicTypeDragNodeObject { + return obj && obj.type === IPublicEnumDragObjectType.Node; } -export interface DragNodeObject { - type: DragObjectType.Node; - nodes: Node[]; -} -export interface DragNodeDataObject { - type: DragObjectType.NodeData; - data: NodeSchema | NodeSchema[]; - thumbnail?: string; - description?: string; - [extra: string]: any; -} - -export interface DragAnyObject { - type: string; - [key: string]: any; -} - -export function isDragNodeObject(obj: any): obj is DragNodeObject { - return obj && obj.type === DragObjectType.Node; -} - -export function isDragNodeDataObject(obj: any): obj is DragNodeDataObject { - return obj && obj.type === DragObjectType.NodeData; +/** + * @deprecated use same function in @alilc/lowcode-utils + */ +export function isDragNodeDataObject(obj: any): obj is IPublicTypeDragNodeDataObject { + return obj && obj.type === IPublicEnumDragObjectType.NodeData; } -export function isDragAnyObject(obj: any): obj is DragAnyObject { - return obj && obj.type !== DragObjectType.NodeData && obj.type !== DragObjectType.Node; +/** + * @deprecated use same function in @alilc/lowcode-utils + */ +export function isDragAnyObject(obj: any): obj is IPublicTypeDragAnyObject { + return obj && obj.type !== IPublicEnumDragObjectType.NodeData && obj.type !== IPublicEnumDragObjectType.Node; } -export function isLocateEvent(e: any): e is LocateEvent { +export function isLocateEvent(e: any): e is ILocateEvent { return e && e.type === 'LocateEvent'; } const SHAKE_DISTANCE = 4; + /** * mouse shake check */ @@ -153,29 +84,40 @@ export function setShaken(e: any) { e.shaken = true; } -function getSourceSensor(dragObject: DragObject): ISimulatorHost | null { +function getSourceSensor(dragObject: IPublicModelDragObject): ISimulatorHost | null { if (!isDragNodeObject(dragObject)) { return null; } - return dragObject.nodes[0]?.document.simulator || null; + return dragObject.nodes[0]?.document?.simulator || null; } function isDragEvent(e: any): e is DragEvent { return e?.type?.startsWith('drag'); } +export interface IDragon extends IPublicModelDragon< + INode, + ILocateEvent +> { + emitter: IEventBus; +} + /** * Drag-on 拖拽引擎 */ -export class Dragon { - private sensors: ISensor[] = []; +export class Dragon implements IDragon { + private sensors: IPublicModelSensor[] = []; + + private nodeInstPointerEvents: boolean; + + key = Math.random(); /** * current active sensor, 可用于感应区高亮 */ - @obx.ref private _activeSensor: ISensor | undefined; + @obx.ref private _activeSensor: IPublicModelSensor | undefined; - get activeSensor(): ISensor | undefined { + get activeSensor(): IPublicModelSensor | undefined { return this._activeSensor; } @@ -187,10 +129,13 @@ export class Dragon { return this._dragging; } - private emitter = new EventEmitter(); + viewName: string | undefined; + + emitter: IEventBus = createModuleEventBus('Dragon'); - constructor(readonly designer: Designer) { + constructor(readonly designer: IDesigner) { makeObservable(this); + this.viewName = designer.viewName; } /** @@ -198,7 +143,7 @@ export class Dragon { * @param shell container element * @param boost boost got a drag object */ - from(shell: Element, boost: (e: MouseEvent) => DragObject | null) { + from(shell: Element, boost: (e: MouseEvent) => IPublicModelDragObject | null) { const mousedown = (e: MouseEvent) => { // ESC or RightClick if (e.which === 3 || e.button === 2) { @@ -225,15 +170,15 @@ export class Dragon { * @param dragObject 拖拽对象 * @param boostEvent 拖拽初始时事件 */ - boost(dragObject: DragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: Node) { + boost(dragObject: IPublicModelDragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: INode | IPublicModelNode) { const { designer } = this; const masterSensors = this.getMasterSensors(); const handleEvents = makeEventsHandler(boostEvent, masterSensors); const newBie = !isDragNodeObject(dragObject); const forceCopyState = - isDragNodeObject(dragObject) && dragObject.nodes.some((node) => node.isSlot()); + isDragNodeObject(dragObject) && dragObject.nodes.some((node: Node | IPublicModelNode) => (typeof node.isSlot === 'function' ? node.isSlot() : node.isSlot)); const isBoostFromDragAPI = isDragEvent(boostEvent); - let lastSensor: ISensor | undefined; + let lastSensor: IPublicModelSensor | undefined; this._dragging = false; @@ -322,7 +267,7 @@ export class Dragon { this.emitter.emit('rgl.add.placeholder', { rglNode, fromRglNode, - node: locateEvent.dragObject.nodes[0], + node: locateEvent.dragObject?.nodes[0], event: e, }); designer.clearLocation(); @@ -407,7 +352,7 @@ export class Dragon { if (e) { const { isRGL, rglNode } = getRGL(e); /* istanbul ignore next */ - if (isRGL && this._canDrop) { + if (isRGL && this._canDrop && this._dragging) { const tarNode = dragObject.nodes[0]; if (rglNode.id !== tarNode.id) { // 避免死循环 @@ -415,8 +360,8 @@ export class Dragon { rglNode, node: tarNode, }); - const { selection } = designer.project.currentDocument; - selection.select(tarNode.id); + const selection = designer.project.currentDocument?.selection; + selection?.select(tarNode.id); } } } @@ -474,7 +419,7 @@ export class Dragon { }; // create drag locate event - const createLocateEvent = (e: MouseEvent | DragEvent): LocateEvent => { + const createLocateEvent = (e: MouseEvent | DragEvent): ILocateEvent => { const evt: any = { type: 'LocateEvent', dragObject, @@ -520,9 +465,9 @@ export class Dragon { const sourceSensor = getSourceSensor(dragObject); /* istanbul ignore next */ - const chooseSensor = (e: LocateEvent) => { + const chooseSensor = (e: ILocateEvent) => { // this.sensors will change on dragstart - const sensors: ISensor[] = this.sensors.concat(masterSensors as ISensor[]); + const sensors: IPublicModelSensor[] = this.sensors.concat(masterSensors as IPublicModelSensor[]); let sensor = e.sensor && e.sensor.isEnter(e) ? e.sensor @@ -671,21 +616,21 @@ export class Dragon { } } - onDragstart(func: (e: LocateEvent) => any) { + onDragstart(func: (e: ILocateEvent) => any) { this.emitter.on('dragstart', func); return () => { this.emitter.removeListener('dragstart', func); }; } - onDrag(func: (e: LocateEvent) => any) { + onDrag(func: (e: ILocateEvent) => any) { this.emitter.on('drag', func); return () => { this.emitter.removeListener('drag', func); }; } - onDragend(func: (x: { dragObject: DragObject; copy: boolean }) => any) { + onDragend(func: (x: { dragObject: IPublicModelDragObject; copy: boolean }) => any) { this.emitter.on('dragend', func); return () => { this.emitter.removeListener('dragend', func); diff --git a/packages/designer/src/designer/focusing.ts b/packages/designer/src/designer/focusing.ts deleted file mode 100644 index 66816bc03c..0000000000 --- a/packages/designer/src/designer/focusing.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Designer } from './designer'; - -// TODO: use focus-tracker replace -class Focusing { - focusDesigner?: Designer; -} - -export const focusing = new Focusing(); diff --git a/packages/designer/src/designer/index.ts b/packages/designer/src/designer/index.ts index 79982c45b1..34d7a8c09f 100644 --- a/packages/designer/src/designer/index.ts +++ b/packages/designer/src/designer/index.ts @@ -1,5 +1,3 @@ -import './builtin-hotkey'; - export * from './designer'; export * from './designer-view'; export * from './dragon'; @@ -8,3 +6,6 @@ export * from './location'; export * from './offset-observer'; export * from './scroller'; export * from './setting'; +export * from './active-tracker'; +export * from '../document'; +export * from './clipboard'; diff --git a/packages/designer/src/designer/location.ts b/packages/designer/src/designer/location.ts index 84e2a7be56..3b9b080cd6 100644 --- a/packages/designer/src/designer/location.ts +++ b/packages/designer/src/designer/location.ts @@ -1,43 +1,13 @@ -import { DocumentModel, Node as ComponentNode, ParentalNode } from '../document'; -import { LocateEvent } from './dragon'; - -export interface LocationData { - target: ParentalNode; // shadowNode | ConditionFlow | ElementNode | RootNode - detail: LocationDetail; - source: string; - event: LocateEvent; -} - -export enum LocationDetailType { - Children = 'Children', - Prop = 'Prop', -} - -export interface LocationChildrenDetail { - type: LocationDetailType.Children; - index?: number | null; - /** - * 是否有效位置 - */ - valid?: boolean; - edge?: DOMRect; - near?: { - node: ComponentNode; - pos: 'before' | 'after' | 'replace'; - rect?: Rect; - align?: 'V' | 'H'; - }; - focus?: { type: 'slots' } | { type: 'node'; node: ParentalNode }; -} - -export interface LocationPropDetail { - // cover 形态,高亮 domNode,如果 domNode 为空,取 container 的值 - type: LocationDetailType.Prop; - name: string; - domNode?: HTMLElement; -} - -export type LocationDetail = LocationChildrenDetail | LocationPropDetail | { type: string; [key: string]: any }; +import type { IDocumentModel, INode } from '../document'; +import { ILocateEvent } from './dragon'; +import { + IPublicModelDropLocation, + IPublicTypeLocationDetailType, + IPublicTypeRect, + IPublicTypeLocationDetail, + IPublicTypeLocationData, + IPublicModelLocateEvent, +} from '@alilc/lowcode-types'; export interface Point { clientX: number; @@ -53,17 +23,18 @@ export type Rects = DOMRect[] & { elements: Array<Element | Text>; }; -export type Rect = DOMRect & { - elements: Array<Element | Text>; - computed?: boolean; -}; - -export function isLocationData(obj: any): obj is LocationData { +/** + * @deprecated use same function in @alilc/lowcode-utils + */ +export function isLocationData(obj: any): boolean { return obj && obj.target && obj.detail; } -export function isLocationChildrenDetail(obj: any): obj is LocationChildrenDetail { - return obj && obj.type === LocationDetailType.Children; +/** + * @deprecated use same function in @alilc/lowcode-utils + */ +export function isLocationChildrenDetail(obj: any): boolean { + return obj && obj.type === IPublicTypeLocationDetailType.Children; } export function isRowContainer(container: Element | Text, win?: Window) { @@ -92,7 +63,7 @@ export function isChildInline(child: Element | Text, win?: Window) { return /^inline/.test(style.getPropertyValue('display')) || /^(left|right)$/.test(style.getPropertyValue('float')); } -export function getRectTarget(rect: Rect | null) { +export function getRectTarget(rect: IPublicTypeRect | null) { if (!rect || rect.computed) { return null; } @@ -100,7 +71,7 @@ export function getRectTarget(rect: Rect | null) { return els && els.length > 0 ? els[0]! : null; } -export function isVerticalContainer(rect: Rect | null) { +export function isVerticalContainer(rect: IPublicTypeRect | null) { const el = getRectTarget(rect); if (!el) { return false; @@ -108,7 +79,7 @@ export function isVerticalContainer(rect: Rect | null) { return isRowContainer(el); } -export function isVertical(rect: Rect | null) { +export function isVertical(rect: IPublicTypeRect | null) { const el = getRectTarget(rect); if (!el) { return false; @@ -127,28 +98,38 @@ function isDocument(elem: any): elem is Document { export function getWindow(elem: Element | Document): Window { return (isDocument(elem) ? elem : elem.ownerDocument!).defaultView!; } +export interface IDropLocation extends Omit<IPublicModelDropLocation, 'target' | 'clone'> { + + readonly source: string; + + get target(): INode; + + get document(): IDocumentModel | null; + + clone(event: IPublicModelLocateEvent): IDropLocation; +} -export class DropLocation { - readonly target: ParentalNode; +export class DropLocation implements IDropLocation { + readonly target: INode; - readonly detail: LocationDetail; + readonly detail: IPublicTypeLocationDetail; - readonly event: LocateEvent; + readonly event: ILocateEvent; readonly source: string; - get document(): DocumentModel { + get document(): IDocumentModel | null { return this.target.document; } - constructor({ target, detail, source, event }: LocationData) { + constructor({ target, detail, source, event }: IPublicTypeLocationData<INode>) { this.target = target; this.detail = detail; this.source = source; this.event = event; } - clone(event: LocateEvent): DropLocation { + clone(event: ILocateEvent): IDropLocation { return new DropLocation({ target: this.target, detail: this.detail, @@ -177,7 +158,7 @@ export class DropLocation { if (this.detail.index <= 0) { return null; } - return this.target.children.get(this.detail.index - 1); + return this.target.children?.get(this.detail.index - 1); } return (this.detail as any)?.near?.node; } diff --git a/packages/designer/src/designer/offset-observer.ts b/packages/designer/src/designer/offset-observer.ts index fde87e1ff5..2cf5bfee26 100644 --- a/packages/designer/src/designer/offset-observer.ts +++ b/packages/designer/src/designer/offset-observer.ts @@ -1,7 +1,8 @@ +import requestIdleCallback, { cancelIdleCallback } from 'ric-shim'; import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core'; import { uniqueId } from '@alilc/lowcode-utils'; import { INodeSelector, IViewport } from '../simulator'; -import { isRootNode, Node } from '../document'; +import { INode } from '../document'; export class OffsetObserver { readonly id = uniqueId('oobx'); @@ -92,11 +93,11 @@ export class OffsetObserver { private pid: number | undefined; - readonly viewport: IViewport; + readonly viewport: IViewport | undefined; private isRoot: boolean; - readonly node: Node; + readonly node: INode; readonly compute: () => void; @@ -104,10 +105,10 @@ export class OffsetObserver { const { node, instance } = nodeInstance; this.node = node; const doc = node.document; - const host = doc.simulator!; - const focusNode = doc.focusNode; + const host = doc?.simulator; + const focusNode = doc?.focusNode; this.isRoot = node.contains(focusNode!); - this.viewport = host.viewport; + this.viewport = host?.viewport; makeObservable(this); if (this.isRoot) { this.hasOffset = true; @@ -117,7 +118,7 @@ export class OffsetObserver { return; } - let pid: number; + let pid: number | undefined; const compute = () => { if (pid !== this.pid) { return; @@ -136,7 +137,7 @@ export class OffsetObserver { this._bottom = rect.bottom; this.hasOffset = true; } - this.pid = (window as any).requestIdleCallback(compute); + this.pid = requestIdleCallback(compute); pid = this.pid; }; @@ -145,13 +146,13 @@ export class OffsetObserver { // try first compute(); // try second, ensure the dom mounted - this.pid = (window as any).requestIdleCallback(compute); + this.pid = requestIdleCallback(compute); pid = this.pid; } purge() { if (this.pid) { - (window as any).cancelIdleCallback(this.pid); + cancelIdleCallback(this.pid); } this.pid = undefined; } diff --git a/packages/designer/src/designer/scroller.ts b/packages/designer/src/designer/scroller.ts index 4ad3785454..7391c39fab 100644 --- a/packages/designer/src/designer/scroller.ts +++ b/packages/designer/src/designer/scroller.ts @@ -1,6 +1,10 @@ import { isElement } from '@alilc/lowcode-utils'; +import { IPublicModelScrollTarget, IPublicTypeScrollable, IPublicModelScroller } from '@alilc/lowcode-types'; -export class ScrollTarget { +export interface IScrollTarget extends IPublicModelScrollTarget { +} + +export class ScrollTarget implements IScrollTarget { get left() { return 'scrollX' in this.target ? this.target.scrollX : this.target.scrollLeft; } @@ -9,6 +13,14 @@ export class ScrollTarget { return 'scrollY' in this.target ? this.target.scrollY : this.target.scrollTop; } + private doc?: HTMLElement; + + constructor(private target: Window | Element) { + if (isWindow(target)) { + this.doc = target.document.documentElement; + } + } + scrollTo(options: { left?: number; top?: number }) { this.target.scrollTo(options); } @@ -24,14 +36,6 @@ export class ScrollTarget { get scrollWidth(): number { return ((this.doc || this.target) as any).scrollWidth; } - - private doc?: HTMLElement; - - constructor(private target: Window | Element) { - if (isWindow(target)) { - this.doc = target.document.documentElement; - } - } } function isWindow(obj: any): obj is Window { @@ -42,20 +46,20 @@ function easing(n: number) { return Math.sin((n * Math.PI) / 2); } -const SCROLL_ACCURCY = 30; +const SCROLL_ACCURACY = 30; -export interface IScrollable { - scrollTarget?: ScrollTarget | Element; - bounds?: DOMRect | null; - scale?: number; -} +export interface IScroller extends IPublicModelScroller { -export class Scroller { +} +export class Scroller implements IScroller { private pid: number | undefined; + scrollable: IPublicTypeScrollable; - constructor(private scrollable: IScrollable) {} + constructor(scrollable: IPublicTypeScrollable) { + this.scrollable = scrollable; + } - get scrollTarget(): ScrollTarget | null { + get scrollTarget(): IScrollTarget | null { let target = this.scrollable.scrollTarget; if (!target) { return null; @@ -138,15 +142,15 @@ export class Scroller { let sy = scrollTarget.top; let ax = 0; let ay = 0; - if (y < bounds.top + SCROLL_ACCURCY) { - ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale; - } else if (y > bounds.bottom - SCROLL_ACCURCY) { - ay = Math.min(Math.max(y + SCROLL_ACCURCY - bounds.bottom, 10), 50) / scale; + if (y < bounds.top + SCROLL_ACCURACY) { + ay = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale; + } else if (y > bounds.bottom - SCROLL_ACCURACY) { + ay = Math.min(Math.max(y + SCROLL_ACCURACY - bounds.bottom, 10), 50) / scale; } - if (x < bounds.left + SCROLL_ACCURCY) { - ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURCY - y, 10), 50) / scale; - } else if (x > bounds.right - SCROLL_ACCURCY) { - ax = Math.min(Math.max(x + SCROLL_ACCURCY - bounds.right, 10), 50) / scale; + if (x < bounds.left + SCROLL_ACCURACY) { + ax = -Math.min(Math.max(bounds.top + SCROLL_ACCURACY - y, 10), 50) / scale; + } else if (x > bounds.right - SCROLL_ACCURACY) { + ax = Math.min(Math.max(x + SCROLL_ACCURACY - bounds.right, 10), 50) / scale; } if (!ax && !ay) { diff --git a/packages/designer/src/designer/setting/index.ts b/packages/designer/src/designer/setting/index.ts index a8319e5b27..6cfa914e62 100644 --- a/packages/designer/src/designer/setting/index.ts +++ b/packages/designer/src/designer/setting/index.ts @@ -1,3 +1,4 @@ export * from './setting-field'; export * from './setting-top-entry'; -export * from './setting-entry'; +export * from './setting-entry-type'; +export * from './setting-prop-entry'; diff --git a/packages/designer/src/designer/setting/setting-entry-type.ts b/packages/designer/src/designer/setting/setting-entry-type.ts new file mode 100644 index 0000000000..1aee9016e5 --- /dev/null +++ b/packages/designer/src/designer/setting/setting-entry-type.ts @@ -0,0 +1,45 @@ +import { IPublicApiSetters, IPublicModelEditor } from '@alilc/lowcode-types'; +import { IDesigner } from '../designer'; +import { INode } from '../../document'; +import { ISettingField } from './setting-field'; + +export interface ISettingEntry { + readonly designer: IDesigner | undefined; + + readonly id: string; + + /** + * 同样类型的节点 + */ + readonly isSameComponent: boolean; + + /** + * 一个 + */ + readonly isSingle: boolean; + + /** + * 多个 + */ + readonly isMultiple: boolean; + + /** + * 编辑器引用 + */ + readonly editor: IPublicModelEditor; + + readonly setters: IPublicApiSetters; + + /** + * 取得子项 + */ + get: (propName: string | number) => ISettingField | null; + + readonly nodes: INode[]; + + // @todo 补充 node 定义 + /** + * 获取 node 中的第一项 + */ + getNode: () => any; +} diff --git a/packages/designer/src/designer/setting/setting-entry.ts b/packages/designer/src/designer/setting/setting-entry.ts deleted file mode 100644 index 12b39d0360..0000000000 --- a/packages/designer/src/designer/setting/setting-entry.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { SettingTarget } from '@alilc/lowcode-types'; -import { ComponentMeta } from '../../component-meta'; -import { Designer } from '../designer'; -import { Node } from '../../document'; - -export interface SettingEntry extends SettingTarget { - readonly nodes: Node[]; - readonly componentMeta: ComponentMeta | null; - readonly designer: Designer; - - // 顶端 - readonly top: SettingEntry; - // 父级 - readonly parent: SettingEntry; - - get: (propName: string | number) => SettingEntry | null; -} diff --git a/packages/designer/src/designer/setting/setting-field.ts b/packages/designer/src/designer/setting/setting-field.ts index a38c1323d0..1a63fb7a40 100644 --- a/packages/designer/src/designer/setting/setting-field.ts +++ b/packages/designer/src/designer/setting/setting-field.ts @@ -1,12 +1,26 @@ -import { TitleContent, isDynamicSetter, SetterType, DynamicSetter, FieldExtraProps, FieldConfig, CustomView, isCustomView } from '@alilc/lowcode-types'; +import { ReactNode } from 'react'; +import { + IPublicTypeTitleContent, + IPublicTypeSetterType, + IPublicTypeDynamicSetter, + IPublicTypeFieldExtraProps, + IPublicTypeFieldConfig, + IPublicTypeCustomView, + IPublicTypeDisposable, + IPublicModelSettingField, + IBaseModelSettingField, +} from '@alilc/lowcode-types'; +import type { + IPublicTypeSetValueOptions, +} from '@alilc/lowcode-types'; import { Transducer } from './utils'; -import { SettingPropEntry } from './setting-prop-entry'; -import { SettingEntry } from './setting-entry'; -import { computed, obx, makeObservable, action } from '@alilc/lowcode-editor-core'; -import { cloneDeep } from '@alilc/lowcode-utils'; -import type { ISetValueOptions } from '../../types'; +import { ISettingPropEntry, SettingPropEntry } from './setting-prop-entry'; +import { computed, obx, makeObservable, action, untracked, intl } from '@alilc/lowcode-editor-core'; +import { cloneDeep, isCustomView, isDynamicSetter, isJSExpression } from '@alilc/lowcode-utils'; +import { ISettingTopEntry } from './setting-top-entry'; +import { IComponentMeta, INode } from '@alilc/lowcode-designer'; -function getSettingFieldCollectorKey(parent: SettingEntry, config: FieldConfig) { +function getSettingFieldCollectorKey(parent: ISettingTopEntry | ISettingField, config: IPublicTypeFieldConfig) { let cur = parent; const path = [config.name]; while (cur !== parent.top) { @@ -18,51 +32,87 @@ function getSettingFieldCollectorKey(parent: SettingEntry, config: FieldConfig) return path.join('.'); } -export class SettingField extends SettingPropEntry implements SettingEntry { +export interface ISettingField extends ISettingPropEntry, Omit<IBaseModelSettingField< + ISettingTopEntry, + ISettingField, + IComponentMeta, + INode +>, 'setValue' | 'key' | 'node'> { + readonly isSettingField: true; + + readonly isRequired: boolean; + + readonly isGroup: boolean; + + extraProps: IPublicTypeFieldExtraProps; + + get items(): Array<ISettingField | IPublicTypeCustomView>; + + get title(): string | ReactNode | undefined; + + get setter(): IPublicTypeSetterType | null; + + get expanded(): boolean; + + get valueState(): number; + + setExpanded(value: boolean): void; + + purge(): void; + + setValue( + val: any, + isHotValue?: boolean, + force?: boolean, + extraOptions?: IPublicTypeSetValueOptions, + ): void; + + clearValue(): void; + + valueChange(options: IPublicTypeSetValueOptions): void; + + createField(config: IPublicTypeFieldConfig): ISettingField; + + onEffect(action: () => void): IPublicTypeDisposable; + + internalToShellField(): IPublicModelSettingField; +} + +export class SettingField extends SettingPropEntry implements ISettingField { readonly isSettingField = true; readonly isRequired: boolean; readonly transducer: Transducer; - private _config: FieldConfig; + private _config: IPublicTypeFieldConfig; + + private hotValue: any; + + parent: ISettingTopEntry | ISettingField; - extraProps: FieldExtraProps; + extraProps: IPublicTypeFieldExtraProps; // ==== dynamic properties ==== - private _title?: TitleContent; + private _title?: IPublicTypeTitleContent; get title() { - // FIXME! intl - return this._title || (typeof this.name === 'number' ? `项目 ${this.name}` : this.name); + return ( + this._title || (typeof this.name === 'number' ? `${intl('Item')} ${this.name}` : this.name) + ); } - private _setter?: SetterType | DynamicSetter; - - @computed get setter(): SetterType | null { - if (!this._setter) { - return null; - } - if (isDynamicSetter(this._setter)) { - const shellThis = this.internalToShellPropEntry(); - return this._setter.call(shellThis, shellThis); - } - return this._setter; - } + private _setter?: IPublicTypeSetterType | IPublicTypeDynamicSetter; @obx.ref private _expanded = true; - get expanded(): boolean { - return this._expanded; - } - - setExpanded(value: boolean) { - this._expanded = value; - } + private _items: Array<ISettingField | IPublicTypeCustomView> = []; - parent: SettingEntry; - - constructor(parent: SettingEntry, config: FieldConfig, settingFieldCollector?: (name: string | number, field: SettingField) => void) { + constructor( + parent: ISettingTopEntry | ISettingField, + config: IPublicTypeFieldConfig, + private settingFieldCollector?: (name: string | number, field: ISettingField) => void, + ) { super(parent, config.name, config.type); makeObservable(this); const { title, items, setter, extraProps, ...rest } = config; @@ -89,17 +139,42 @@ export class SettingField extends SettingPropEntry implements SettingEntry { this.transducer = new Transducer(this, { setter }); } - private _items: Array<SettingField | CustomView> = []; + @computed get setter(): IPublicTypeSetterType | null { + if (!this._setter) { + return null; + } + if (isDynamicSetter(this._setter)) { + return untracked(() => { + const shellThis = this.internalToShellField(); + return (this._setter as IPublicTypeDynamicSetter)?.call(shellThis, shellThis!); + }); + } + return this._setter; + } + + get expanded(): boolean { + return this._expanded; + } + + setExpanded(value: boolean) { + this._expanded = value; + } - get items(): Array<SettingField | CustomView> { + get items(): Array<ISettingField | IPublicTypeCustomView> { return this._items; } - get config(): FieldConfig { + get config(): IPublicTypeFieldConfig { return this._config; } - private initItems(items: Array<FieldConfig | CustomView>, settingFieldCollector?: { (name: string | number, field: SettingField): void; (name: string, field: SettingField): void }) { + private initItems( + items: Array<IPublicTypeFieldConfig | IPublicTypeCustomView>, + settingFieldCollector?: { + (name: string | number, field: ISettingField): void; + (name: string, field: ISettingField): void; + }, + ) { this._items = items.map((item) => { if (isCustomView(item)) { return item; @@ -109,13 +184,14 @@ export class SettingField extends SettingPropEntry implements SettingEntry { } private disposeItems() { - this._items.forEach(item => isSettingField(item) && item.purge()); + this._items.forEach((item) => isSettingField(item) && item.purge()); this._items = []; } // 创建子配置项,通常用于 object/array 类型数据 - createField(config: FieldConfig): SettingField { - return new SettingField(this, config); + createField(config: IPublicTypeFieldConfig): ISettingField { + this.settingFieldCollector?.(getSettingFieldCollectorKey(this.parent, config), this); + return new SettingField(this, config, this.settingFieldCollector); } purge() { @@ -124,15 +200,19 @@ export class SettingField extends SettingPropEntry implements SettingEntry { // ======= compatibles for vision ====== - getConfig<K extends keyof FieldConfig>(configName?: K): FieldConfig[K] | FieldConfig { + getConfig<K extends keyof IPublicTypeFieldConfig>( + configName?: K, + ): IPublicTypeFieldConfig[K] | IPublicTypeFieldConfig { if (configName) { return this.config[configName]; } return this._config; } - getItems(filter?: (item: SettingField | CustomView) => boolean): Array<SettingField | CustomView> { - return this._items.filter(item => { + getItems( + filter?: (item: ISettingField | IPublicTypeCustomView) => boolean, + ): Array<ISettingField | IPublicTypeCustomView> { + return this._items.filter((item) => { if (filter) { return filter(item); } @@ -140,10 +220,13 @@ export class SettingField extends SettingPropEntry implements SettingEntry { }); } - private hotValue: any; - @action - setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) { + setValue( + val: any, + isHotValue?: boolean, + force?: boolean, + extraOptions?: IPublicTypeSetValueOptions, + ) { if (isHotValue) { this.setHotValue(val, extraOptions); return; @@ -178,7 +261,7 @@ export class SettingField extends SettingPropEntry implements SettingEntry { } @action - setHotValue(data: any, options?: ISetValueOptions) { + setHotValue(data: any, options?: IPublicTypeSetValueOptions) { this.hotValue = data; const value = this.transducer.toNative(data); if (options) { @@ -188,11 +271,29 @@ export class SettingField extends SettingPropEntry implements SettingEntry { } if (this.isUseVariable()) { const oldValue = this.getValue(); - this.setValue({ - type: 'JSExpression', - value: oldValue.value, - mock: value, - }, false, false, options); + if (isJSExpression(value)) { + this.setValue( + { + type: 'JSExpression', + value: value.value, + mock: oldValue.mock, + }, + false, + false, + options, + ); + } else { + this.setValue( + { + type: 'JSExpression', + value: oldValue.value, + mock: value, + }, + false, + false, + options, + ); + } } else { this.setValue(value, false, false, options); } @@ -205,11 +306,18 @@ export class SettingField extends SettingPropEntry implements SettingEntry { this.valueChange(options); } - onEffect(action: () => void): () => void { - return this.designer.autorun(action, true); + onEffect(action: () => void): IPublicTypeDisposable { + return this.designer!.autorun(action, true); + } + + internalToShellField() { + return this.designer!.shellModelFactory.createSettingField(this); } } -export function isSettingField(obj: any): obj is SettingField { +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isSettingField(obj: any): obj is ISettingField { return obj && obj.isSettingField; } diff --git a/packages/designer/src/designer/setting/setting-prop-entry.ts b/packages/designer/src/designer/setting/setting-prop-entry.ts index cfd603ea47..d6904f0c82 100644 --- a/packages/designer/src/designer/setting/setting-prop-entry.ts +++ b/packages/designer/src/designer/setting/setting-prop-entry.ts @@ -1,18 +1,46 @@ -import { obx, computed, makeObservable, runInAction } from '@alilc/lowcode-editor-core'; -import { GlobalEvent, IEditor, isJSExpression } from '@alilc/lowcode-types'; -import { uniqueId } from '@alilc/lowcode-utils'; -import { SettingPropEntry as ShellSettingPropEntry } from '@alilc/lowcode-shell'; -import { SettingEntry } from './setting-entry'; -import { Node } from '../../document'; -import { ComponentMeta } from '../../component-meta'; -import { Designer } from '../designer'; -import { EventEmitter } from 'events'; -import { ISetValueOptions } from '../../types'; -import { isSettingField } from './setting-field'; - -export class SettingPropEntry implements SettingEntry { +import { obx, computed, makeObservable, runInAction, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { GlobalEvent, IPublicApiSetters, IPublicModelEditor, IPublicModelSettingField, IPublicTypeFieldExtraProps, IPublicTypeSetValueOptions } from '@alilc/lowcode-types'; +import { uniqueId, isJSExpression } from '@alilc/lowcode-utils'; +import { ISettingEntry } from './setting-entry-type'; +import { INode } from '../../document'; +import type { IComponentMeta } from '../../component-meta'; +import { IDesigner } from '../designer'; +import { ISettingTopEntry } from './setting-top-entry'; +import { ISettingField, isSettingField } from './setting-field'; + +export interface ISettingPropEntry extends ISettingEntry { + readonly isGroup: boolean; + + get props(): ISettingTopEntry; + + get name(): string | number | undefined; + + valueChange(options: IPublicTypeSetValueOptions): void; + + getKey(): string | number | undefined; + + setKey(key: string | number): void; + + getDefaultValue(): any; + + setUseVariable(flag: boolean): void; + + getProps(): ISettingTopEntry; + + isUseVariable(): boolean; + + getMockOrValue(): any; + + remove(): void; + + setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: IPublicTypeSetValueOptions): void; + + internalToShellField(): IPublicModelSettingField; +} + +export class SettingPropEntry implements ISettingPropEntry { // === static properties === - readonly editor: IEditor; + readonly editor: IPublicModelEditor; readonly isSameComponent: boolean; @@ -20,13 +48,15 @@ export class SettingPropEntry implements SettingEntry { readonly isSingle: boolean; - readonly nodes: Node[]; + readonly setters: IPublicApiSetters; + + readonly nodes: INode[]; - readonly componentMeta: ComponentMeta | null; + readonly componentMeta: IComponentMeta | null; - readonly designer: Designer; + readonly designer: IDesigner | undefined; - readonly top: SettingEntry; + readonly top: ISettingTopEntry; readonly isGroup: boolean; @@ -34,10 +64,10 @@ export class SettingPropEntry implements SettingEntry { readonly id = uniqueId('entry'); - readonly emitter = new EventEmitter(); + readonly emitter: IEventBus = createModuleEventBus('SettingPropEntry'); // ==== dynamic properties ==== - @obx.ref private _name: string | number; + @obx.ref private _name: string | number | undefined; get name() { return this._name; @@ -45,15 +75,15 @@ export class SettingPropEntry implements SettingEntry { @computed get path() { const path = this.parent.path.slice(); - if (this.type === 'field') { + if (this.type === 'field' && this.name?.toString()) { path.push(this.name); } return path; } - extraProps: any = {}; + extraProps: IPublicTypeFieldExtraProps = {}; - constructor(readonly parent: SettingEntry, name: string | number, type?: 'field' | 'group') { + constructor(readonly parent: ISettingTopEntry | ISettingField, name: string | number | undefined, type?: 'field' | 'group') { makeObservable(this); if (type == null) { const c = typeof name === 'string' ? name.slice(0, 1) : ''; @@ -72,6 +102,7 @@ export class SettingPropEntry implements SettingEntry { // copy parent static properties this.editor = parent.editor; this.nodes = parent.nodes; + this.setters = parent.setters; this.componentMeta = parent.componentMeta; this.isSameComponent = parent.isSameComponent; this.isMultiple = parent.isMultiple; @@ -126,7 +157,7 @@ export class SettingPropEntry implements SettingEntry { if (this.type !== 'field') { const { getValue } = this.extraProps; return getValue - ? getValue(this.internalToShellPropEntry(), undefined) === undefined + ? getValue(this.internalToShellField()!, undefined) === undefined ? 0 : 1 : 0; @@ -160,12 +191,12 @@ export class SettingPropEntry implements SettingEntry { */ getValue(): any { let val: any; - if (this.type === 'field') { + if (this.type === 'field' && this.name?.toString()) { val = this.parent.getPropValue(this.name); } const { getValue } = this.extraProps; try { - return getValue ? getValue(this.internalToShellPropEntry(), val) : val; + return getValue ? getValue(this.internalToShellField()!, val) : val; } catch (e) { console.warn(e); return val; @@ -175,16 +206,16 @@ export class SettingPropEntry implements SettingEntry { /** * 设置当前属性值 */ - setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: ISetValueOptions) { + setValue(val: any, isHotValue?: boolean, force?: boolean, extraOptions?: IPublicTypeSetValueOptions) { const oldValue = this.getValue(); if (this.type === 'field') { - this.parent.setPropValue(this.name, val); + this.name?.toString() && this.parent.setPropValue(this.name, val); } const { setValue } = this.extraProps; if (setValue && !extraOptions?.disableMutator) { try { - setValue(this.internalToShellPropEntry(), val); + setValue(this.internalToShellField()!, val); } catch (e) { /* istanbul ignore next */ console.warn(e); @@ -202,12 +233,12 @@ export class SettingPropEntry implements SettingEntry { */ clearValue() { if (this.type === 'field') { - this.parent.clearPropValue(this.name); + this.name?.toString() && this.parent.clearPropValue(this.name); } const { setValue } = this.extraProps; if (setValue) { try { - setValue(this.internalToShellPropEntry(), undefined); + setValue(this.internalToShellField()!, undefined); } catch (e) { /* istanbul ignore next */ console.warn(e); @@ -289,7 +320,7 @@ export class SettingPropEntry implements SettingEntry { /** * @deprecated */ - valueChange(options: ISetValueOptions = {}) { + valueChange(options: IPublicTypeSetValueOptions = {}) { this.emitter.emit('valuechange', options); if (this.parent && isSettingField(this.parent)) { @@ -298,7 +329,7 @@ export class SettingPropEntry implements SettingEntry { } notifyValueChange(oldValue: any, newValue: any) { - this.editor.emit(GlobalEvent.Node.Prop.Change, { + this.editor.eventBus.emit(GlobalEvent.Node.Prop.Change, { node: this.getNode(), prop: this, oldValue, @@ -363,7 +394,7 @@ export class SettingPropEntry implements SettingEntry { return v; } - internalToShellPropEntry() { - return ShellSettingPropEntry.create(this) as any; + internalToShellField(): IPublicModelSettingField { + return this.designer!.shellModelFactory.createSettingField(this); } } diff --git a/packages/designer/src/designer/setting/setting-top-entry.ts b/packages/designer/src/designer/setting/setting-top-entry.ts index e51f762092..85be74b7f6 100644 --- a/packages/designer/src/designer/setting/setting-top-entry.ts +++ b/packages/designer/src/designer/setting/setting-top-entry.ts @@ -1,30 +1,50 @@ -import { EventEmitter } from 'events'; -import { CustomView, isCustomView, IEditor } from '@alilc/lowcode-types'; -import { computed } from '@alilc/lowcode-editor-core'; -import { SettingEntry } from './setting-entry'; -import { SettingField } from './setting-field'; -import { SettingPropEntry } from './setting-prop-entry'; -import { Node } from '../../document'; -import { ComponentMeta } from '../../component-meta'; -import { Designer } from '../designer'; - -function generateSessionId(nodes: Node[]) { +import { IPublicTypeCustomView, IPublicModelEditor, IPublicModelSettingTopEntry, IPublicApiSetters } from '@alilc/lowcode-types'; +import { isCustomView } from '@alilc/lowcode-utils'; +import { computed, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { ISettingEntry } from './setting-entry-type'; +import { ISettingField, SettingField } from './setting-field'; +import { INode } from '../../document'; +import type { IComponentMeta } from '../../component-meta'; +import { IDesigner } from '../designer'; + +function generateSessionId(nodes: INode[]) { return nodes .map((node) => node.id) .sort() .join(','); } -export class SettingTopEntry implements SettingEntry { - private emitter = new EventEmitter(); +export interface ISettingTopEntry extends ISettingEntry, IPublicModelSettingTopEntry< + INode, + ISettingField +> { + readonly top: ISettingTopEntry; - private _items: Array<SettingField | CustomView> = []; + readonly parent: ISettingTopEntry; - private _componentMeta: ComponentMeta | null = null; + readonly path: never[]; + + items: Array<ISettingField | IPublicTypeCustomView>; + + componentMeta: IComponentMeta | null; + + purge(): void; + + getExtraPropValue(propName: string): void; + + setExtraPropValue(propName: string, value: any): void; +} + +export class SettingTopEntry implements ISettingTopEntry { + private emitter: IEventBus = createModuleEventBus('SettingTopEntry'); + + private _items: Array<SettingField | IPublicTypeCustomView> = []; + + private _componentMeta: IComponentMeta | null = null; private _isSame = true; - private _settingFieldMap: { [prop: string]: SettingField } = {}; + private _settingFieldMap: { [prop: string]: ISettingField } = {}; readonly path = []; @@ -67,19 +87,22 @@ export class SettingTopEntry implements SettingEntry { readonly id: string; - readonly first: Node; + readonly first: INode; + + readonly designer: IDesigner | undefined; - readonly designer: Designer; + readonly setters: IPublicApiSetters; disposeFunctions: any[] = []; - constructor(readonly editor: IEditor, readonly nodes: Node[]) { + constructor(readonly editor: IPublicModelEditor, readonly nodes: INode[]) { if (!Array.isArray(nodes) || nodes.length < 1) { throw new ReferenceError('nodes should not be empty'); } this.id = generateSessionId(nodes); this.first = nodes[0]; - this.designer = this.first.document.designer; + this.designer = this.first.document?.designer; + this.setters = editor.get('setters') as IPublicApiSetters; // setups this.setupComponentMeta(); @@ -114,8 +137,8 @@ export class SettingTopEntry implements SettingEntry { private setupItems() { if (this.componentMeta) { - const settingFieldMap: { [prop: string]: SettingField } = {}; - const settingFieldCollector = (name: string | number, field: SettingField) => { + const settingFieldMap: { [prop: string]: ISettingField } = {}; + const settingFieldCollector = (name: string | number, field: ISettingField) => { settingFieldMap[name] = field; }; this._items = this.componentMeta.configure.map((item) => { @@ -152,34 +175,34 @@ export class SettingTopEntry implements SettingEntry { /** * 获取子项 */ - get(propName: string | number): SettingPropEntry | null { + get(propName: string | number): ISettingField | null { if (!propName) return null; - return this._settingFieldMap[propName] || (new SettingPropEntry(this, propName)); + return this._settingFieldMap[propName] || (new SettingField(this, { name: propName })); } /** * 设置子级属性值 */ - setPropValue(propName: string, value: any) { + setPropValue(propName: string | number, value: any) { this.nodes.forEach((node) => { - node.setPropValue(propName, value); + node.setPropValue(propName.toString(), value); }); } /** * 清除已设置值 */ - clearPropValue(propName: string) { + clearPropValue(propName: string | number) { this.nodes.forEach((node) => { - node.clearPropValue(propName); + node.clearPropValue(propName.toString()); }); } /** * 获取子级属性值 */ - getPropValue(propName: string): any { - return this.first.getProp(propName, true)?.getValue(); + getPropValue(propName: string | number): any { + return this.first.getProp(propName.toString(), true)?.getValue(); } /** @@ -225,7 +248,6 @@ export class SettingTopEntry implements SettingEntry { this.disposeFunctions = []; } - getProp(propName: string | number) { return this.get(propName); } diff --git a/packages/designer/src/designer/setting/utils.ts b/packages/designer/src/designer/setting/utils.ts index 0958b7a780..75ed1dfc1a 100644 --- a/packages/designer/src/designer/setting/utils.ts +++ b/packages/designer/src/designer/setting/utils.ts @@ -1,8 +1,8 @@ // all this file for polyfill vision logic import { isValidElement } from 'react'; -import { isSetterConfig, isDynamicSetter, FieldConfig, SetterConfig } from '@alilc/lowcode-types'; -import { getSetter } from '@alilc/lowcode-editor-core'; -import { SettingField } from './setting-field'; +import { IPublicTypeFieldConfig, IPublicTypeSetterConfig } from '@alilc/lowcode-types'; +import { isSetterConfig, isDynamicSetter } from '@alilc/lowcode-utils'; +import { ISettingField } from './setting-field'; function getHotterFromSetter(setter) { return setter && (setter.Hotter || (setter.type && setter.type.Hotter)) || []; // eslint-disable-line @@ -35,12 +35,12 @@ export class Transducer { context: any; - constructor(context: SettingField, config: { setter: FieldConfig['setter'] }) { + constructor(context: ISettingField, config: { setter: IPublicTypeFieldConfig['setter'] }) { let { setter } = config; // 1. validElement - // 2. SetterConfig - // 3. SetterConfig[] + // 2. IPublicTypeSetterConfig + // 3. IPublicTypeSetterConfig[] if (Array.isArray(setter)) { setter = setter[0]; } else if (isValidElement(setter) && setter.type.displayName === 'MixedSetter') { @@ -58,19 +58,19 @@ export class Transducer { let isDynamic = true; if (isSetterConfig(setter)) { - const { componentName, isDynamic: dynamicFlag } = setter as SetterConfig; + const { componentName, isDynamic: dynamicFlag } = setter as IPublicTypeSetterConfig; setter = componentName; isDynamic = dynamicFlag !== false; } if (typeof setter === 'string') { - const { component, isDynamic: dynamicFlag } = getSetter(setter) || {}; + const { component, isDynamic: dynamicFlag } = context.setters.getSetter(setter) || {}; setter = component; // 如果在物料配置中声明了,在 registerSetter 没有声明,取物料配置中的声明 isDynamic = dynamicFlag === undefined ? isDynamic : dynamicFlag !== false; } 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/document-model.ts b/packages/designer/src/document/document-model.ts index 11c32fc42e..edca8fd818 100644 --- a/packages/designer/src/document/document-model.ts +++ b/packages/designer/src/document/document-model.ts @@ -1,29 +1,163 @@ -import { makeObservable, obx, engineConfig, action, runWithGlobalEventOff, wrapWithEventSwitch } from '@alilc/lowcode-editor-core'; -import { NodeData, isJSExpression, isDOMText, NodeSchema, isNodeSchema, RootSchema, PageSchema, ComponentsMap } from '@alilc/lowcode-types'; -import { EventEmitter } from 'events'; -import { Project } from '../project'; +import { + makeObservable, + obx, + engineConfig, + action, + runWithGlobalEventOff, + wrapWithEventSwitch, + createModuleEventBus, + IEventBus, +} from '@alilc/lowcode-editor-core'; +import { + IPublicTypeNodeData, + IPublicTypeNodeSchema, + IPublicTypePageSchema, + IPublicTypeComponentsMap, + IPublicTypeDragNodeObject, + IPublicTypeDragNodeDataObject, + IPublicModelDocumentModel, + IPublicEnumTransformStage, + IPublicTypeOnChangeOptions, + IPublicTypeDisposable, +} from '@alilc/lowcode-types'; +import type { + IPublicTypeRootSchema, +} from '@alilc/lowcode-types'; +import type { + IDropLocation, +} from '@alilc/lowcode-designer'; +import { + uniqueId, + isPlainObject, + compatStage, + isJSExpression, + isDOMText, + isNodeSchema, + isDragNodeObject, + isDragNodeDataObject, + isNode, +} from '@alilc/lowcode-utils'; +import { IProject } from '../project'; import { ISimulatorHost } from '../simulator'; -import { ComponentMeta } from '../component-meta'; -import { isDragNodeDataObject, DragNodeObject, DragNodeDataObject, DropLocation, Designer } from '../designer'; -import { Node, insertChildren, insertChild, isNode, RootNode, ParentalNode } from './node/node'; -import { Selection } from './selection'; +import type { IComponentMeta } from '../component-meta'; +import { IDesigner, IHistory } from '../designer'; +import { insertChildren, insertChild, IRootNode } from './node/node'; +import type { INode } from './node/node'; +import { Selection, ISelection } from './selection'; import { History } from './history'; -import { TransformStage, ModalNodesManager } from './node'; -import { uniqueId, isPlainObject, compatStage } from '@alilc/lowcode-utils'; +import { IModalNodesManager, ModalNodesManager, Node } from './node'; +import { EDITOR_EVENT } from '../types'; export type GetDataType<T, NodeType> = T extends undefined ? NodeType extends { schema: infer R; } - ? R - : any + ? R + : any : T; -export class DocumentModel { +export interface IDocumentModel extends Omit<IPublicModelDocumentModel< + ISelection, + IHistory, + INode, + IDropLocation, + IModalNodesManager, + IProject +>, + 'detecting' | + 'checkNesting' | + 'getNodeById' | + // 以下属性在内部的 document 中不存在 + 'exportSchema' | + 'importSchema' | + 'onAddNode' | + 'onRemoveNode' | + 'onChangeDetecting' | + 'onChangeSelection' | + 'onChangeNodeProp' | + 'onImportSchema' | + 'isDetectingNode' | + 'onFocusNodeChanged' | + 'onDropLocationChanged' +> { + + readonly designer: IDesigner; + + selection: ISelection; + + get rootNode(): INode | null; + + get simulator(): ISimulatorHost | null; + + get active(): boolean; + + get nodesMap(): Map<string, INode>; + + /** + * 是否为非激活状态 + */ + get suspensed(): boolean; + + get fileName(): string; + + get currentRoot(): INode | null; + + isBlank(): boolean; + + /** + * 根据 id 获取节点 + */ + getNode(id: string): INode | null; + + getRoot(): INode | null; + + getHistory(): IHistory; + + checkNesting( + dropTarget: INode, + dragObject: IPublicTypeDragNodeObject | IPublicTypeNodeSchema | INode | IPublicTypeDragNodeDataObject, + ): boolean; + + getNodeCount(): number; + + nextId(possibleId: string | undefined): string; + + import(schema: IPublicTypeRootSchema, checkId?: boolean): void; + + export(stage: IPublicEnumTransformStage): IPublicTypeRootSchema | undefined; + + onNodeCreate(func: (node: INode) => void): IPublicTypeDisposable; + + onNodeDestroy(func: (node: INode) => void): IPublicTypeDisposable; + + onChangeNodeVisible(fn: (node: INode, visible: boolean) => void): IPublicTypeDisposable; + + addWillPurge(node: INode): void; + + removeWillPurge(node: INode): void; + + getComponentMeta(componentName: string): IComponentMeta; + + insertNodes(parent: INode, thing: INode[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean): INode[]; + + open(): IDocumentModel; + + remove(): void; + + suspense(): void; + + close(): void; + + unlinkNode(node: INode): void; + + destroyNode(node: INode): void; +} + +export class DocumentModel implements IDocumentModel { /** * 根节点 类型有:Page/Component/Block */ - rootNode: RootNode | null; + rootNode: IRootNode | null; /** * 文档编号 @@ -33,29 +167,29 @@ export class DocumentModel { /** * 选区控制 */ - readonly selection: Selection = new Selection(this); + readonly selection: ISelection = new Selection(this); /** * 操作记录控制 */ - readonly history: History; + readonly history: IHistory; /** * 模态节点管理 */ - readonly modalNodesManager: ModalNodesManager; + modalNodesManager: IModalNodesManager; - private _nodesMap = new Map<string, Node>(); + private _nodesMap = new Map<string, INode>(); - readonly project: Project; + readonly project: IProject; - readonly designer: Designer; + readonly designer: IDesigner; - @obx.shallow private nodes = new Set<Node>(); + @obx.shallow private nodes = new Set<INode>(); private seqId = 0; - private emitter: EventEmitter; + private emitter: IEventBus; private rootNodeVisitorMap: { [visitorName: string]: any } = {}; @@ -71,7 +205,7 @@ export class DocumentModel { return this.project.simulator; } - get nodesMap(): Map<string, Node> { + get nodesMap(): Map<string, INode> { return this._nodesMap; } @@ -83,7 +217,7 @@ export class DocumentModel { this.rootNode?.getExtraProp('fileName', true)?.setValue(fileName); } - get focusNode() { + get focusNode(): INode | null { if (this._drillDownNode) { return this._drillDownNode; } @@ -94,23 +228,92 @@ export class DocumentModel { return this.rootNode; } - @obx.ref private _drillDownNode: Node | null = null; - - drillDown(node: Node | null) { - this._drillDownNode = node; - } + @obx.ref private _drillDownNode: INode | null = null; - private _modalNode?: ParentalNode; + private _modalNode?: INode; private _blank?: boolean; private inited = false; - constructor(project: Project, schema?: RootSchema) { + @obx.shallow private willPurgeSpace: INode[] = []; + + get modalNode() { + return this._modalNode; + } + + get currentRoot() { + return this.modalNode || this.focusNode; + } + + @obx.shallow private activeNodes?: INode[]; + + @obx.ref private _dropLocation: IDropLocation | null = null; + + set dropLocation(loc: IDropLocation | null) { + this._dropLocation = loc; + // pub event + this.designer.editor.eventBus.emit( + 'document.dropLocation.changed', + { document: this, location: loc }, + ); + } + + /** + * 投放插入位置标记 + */ + get dropLocation() { + return this._dropLocation; + } + + /** + * 导出 schema 数据 + */ + get schema(): IPublicTypeRootSchema { + return this.rootNode?.schema as any; + } + + @obx.ref private _opened = false; + + @obx.ref private _suspensed = false; + + /** + * 是否为非激活状态 + */ + get suspensed(): boolean { + return this._suspensed || !this._opened; + } + + /** + * 与 suspensed 相反,是否为激活状态,这个函数可能用的更多一点 + */ + get active(): boolean { + return !this._suspensed; + } + + /** + * @deprecated 兼容 + */ + get actived(): boolean { + return this.active; + } + + /** + * 是否打开 + */ + get opened() { + return this._opened; + } + + get root() { + return this.rootNode; + } + + constructor(project: IProject, schema?: IPublicTypeRootSchema) { makeObservable(this); this.project = project; this.designer = this.project?.designer; - this.emitter = new EventEmitter(); + this.emitter = createModuleEventBus('DocumentModel'); if (!schema) { this._blank = true; @@ -119,7 +322,7 @@ export class DocumentModel { // 兼容 vision this.id = project.getSchema()?.id || this.id; - this.rootNode = this.createNode<RootNode>( + this.rootNode = this.createNode( schema || { componentName: 'Page', id: 'root', @@ -128,11 +331,12 @@ export class DocumentModel { ); this.history = new History( - () => this.export(TransformStage.Serilize), + () => this.export(IPublicEnumTransformStage.Serilize), (schema) => { - this.import(schema as RootSchema, true); + this.import(schema as IPublicTypeRootSchema, true); this.simulator?.rerender(); }, + this, ); this.setupListenActiveNodes(); @@ -140,21 +344,31 @@ export class DocumentModel { this.inited = true; } - @obx.shallow private willPurgeSpace: Node[] = []; + drillDown(node: INode | null) { + this._drillDownNode = node; + } + + onChangeNodeVisible(fn: (node: INode, visible: boolean) => void): IPublicTypeDisposable { + this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn); - get modalNode() { - return this._modalNode; + return () => { + this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_VISIBLE_CHANGE, fn); + }; } - get currentRoot() { - return this.modalNode || this.focusNode; + onChangeNodeChildren(fn: (info: IPublicTypeOnChangeOptions<INode>) => void): IPublicTypeDisposable { + this.designer.editor?.eventBus.on(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn); + + return () => { + this.designer.editor?.eventBus.off(EDITOR_EVENT.NODE_CHILDREN_CHANGE, fn); + }; } - addWillPurge(node: Node) { + addWillPurge(node: INode) { this.willPurgeSpace.push(node); } - removeWillPurge(node: Node) { + removeWillPurge(node: INode) { const i = this.willPurgeSpace.indexOf(node); if (i > -1) { this.willPurgeSpace.splice(i, 1); @@ -162,13 +376,13 @@ export class DocumentModel { } isBlank() { - return this._blank && !this.isModified(); + return !!(this._blank && !this.isModified()); } /** - * 生成唯一id + * 生成唯一 id */ - nextId(possibleId: string | undefined) { + nextId(possibleId: string | undefined): string { let id = possibleId; while (!id || this.nodesMap.get(id)) { id = `node_${(String(this.id).slice(-10) + (++this.seqId).toString(36)).toLocaleLowerCase()}`; @@ -180,7 +394,7 @@ export class DocumentModel { /** * 根据 id 获取节点 */ - getNode(id: string): Node | null { + getNode(id: string): INode | null { return this._nodesMap.get(id) || null; } @@ -199,13 +413,19 @@ export class DocumentModel { return node ? !node.isPurged : false; } - @obx.shallow private activeNodes?: Node[]; + onMountNode(fn: (payload: { node: INode }) => void) { + this.designer.editor.eventBus.on('node.add', fn as any); + + return () => { + this.designer.editor.eventBus.off('node.add', fn as any); + }; + } /** * 根据 schema 创建一个节点 */ @action - createNode<T extends Node = Node, C = undefined>(data: GetDataType<C, T>, checkId: boolean = true): T { + createNode<T extends INode = INode, C = undefined>(data: GetDataType<C, T>): T { let schema: any; if (isDOMText(data) || isJSExpression(data)) { schema = { @@ -216,7 +436,7 @@ export class DocumentModel { schema = data; } - let node: Node | null = null; + let node: INode | null = null; if (this.hasNode(schema?.id)) { schema.id = null; } @@ -236,7 +456,7 @@ export class DocumentModel { } } if (!node) { - node = new Node(this, schema, { checkId }); + node = new Node(this, schema); // will add // todo: this.activeNodes?.push(node); } @@ -248,36 +468,36 @@ export class DocumentModel { return node as any; } - public destroyNode(node: Node) { + public destroyNode(node: INode) { this.emitter.emit('nodedestroy', node); } /** * 插入一个节点 */ - insertNode(parent: ParentalNode, thing: Node | NodeData, at?: number | null, copy?: boolean): Node { + insertNode(parent: INode, thing: INode | IPublicTypeNodeData, at?: number | null, copy?: boolean): INode | null { return insertChild(parent, thing, at, copy); } /** * 插入多个节点 */ - insertNodes(parent: ParentalNode, thing: Node[] | NodeData[], at?: number | null, copy?: boolean) { + insertNodes(parent: INode, thing: INode[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean) { return insertChildren(parent, thing, at, copy); } /** * 移除一个节点 */ - removeNode(idOrNode: string | Node) { + removeNode(idOrNode: string | INode) { let id: string; - let node: Node | null; + let node: INode | null = null; if (typeof idOrNode === 'string') { id = idOrNode; node = this.getNode(id); - } else { - node = idOrNode; - id = node.id; + } else if (idOrNode.id) { + id = idOrNode.id; + node = this.getNode(id); } if (!node) { return; @@ -288,38 +508,22 @@ export class DocumentModel { /** * 内部方法,请勿调用 */ - internalRemoveAndPurgeNode(node: Node, useMutator = false) { + internalRemoveAndPurgeNode(node: INode, useMutator = false) { if (!this.nodes.has(node)) { return; } node.remove(useMutator); } - unlinkNode(node: Node) { + unlinkNode(node: INode) { this.nodes.delete(node); this._nodesMap.delete(node.id); } - @obx.ref private _dropLocation: DropLocation | null = null; - - /** - * 内部方法,请勿调用 - */ - internalSetDropLocation(loc: DropLocation | null) { - this._dropLocation = loc; - } - - /** - * 投放插入位置标记 - */ - get dropLocation() { - return this._dropLocation; - } - /** * 包裹当前选区中的节点 */ - wrapWith(schema: NodeSchema): Node | null { + wrapWith(schema: IPublicTypeNodeSchema): INode | null { const nodes = this.selection.getTopNodes(); if (nodes.length < 1) { return null; @@ -338,15 +542,8 @@ export class DocumentModel { return null; } - /** - * 导出 schema 数据 - */ - get schema(): RootSchema { - return this.rootNode?.schema as any; - } - @action - import(schema: RootSchema, checkId = false) { + import(schema: IPublicTypeRootSchema, checkId = false) { const drillDownNodeId = this._drillDownNode?.id; runWithGlobalEventOff(() => { // TODO: 暂时用饱和式删除,原因是 Slot 节点并不是树节点,无法正常递归删除 @@ -363,17 +560,17 @@ export class DocumentModel { }); } - export(stage: TransformStage = TransformStage.Serilize) { + export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Serilize): IPublicTypeRootSchema | undefined { stage = compatStage(stage); // 置顶只作用于 Page 的第一级子节点,目前还用不到里层的置顶;如果后面有需要可以考虑将这段写到 node-children 中的 export - const currentSchema = this.rootNode?.export(stage); - if (Array.isArray(currentSchema?.children) && currentSchema?.children.length > 0) { - const FixedTopNodeIndex = currentSchema.children + const currentSchema = this.rootNode?.export<IPublicTypeRootSchema>(stage); + if (Array.isArray(currentSchema?.children) && currentSchema?.children?.length && currentSchema?.children?.length > 0) { + const FixedTopNodeIndex = currentSchema?.children .filter(i => isPlainObject(i)) - .findIndex((i => (i as NodeSchema).props?.__isTopFixed__)); + .findIndex((i => (i as IPublicTypeNodeSchema).props?.__isTopFixed__)); if (FixedTopNodeIndex > 0) { - const FixedTopNode = currentSchema.children.splice(FixedTopNodeIndex, 1); - currentSchema.children.unshift(FixedTopNode[0]); + const FixedTopNode = currentSchema?.children.splice(FixedTopNodeIndex, 1); + currentSchema?.children.unshift(FixedTopNode[0]); } } return currentSchema; @@ -382,7 +579,7 @@ export class DocumentModel { /** * 导出节点数据 */ - getNodeSchema(id: string): NodeData | null { + getNodeSchema(id: string): IPublicTypeNodeData | null { const node = this.getNode(id); if (node) { return node.schema; @@ -393,7 +590,7 @@ export class DocumentModel { /** * 是否已修改 */ - isModified() { + isModified(): boolean { return this.history.isSavePoint(); } @@ -402,45 +599,13 @@ export class DocumentModel { return this.simulator!.getComponent(componentName); } - getComponentMeta(componentName: string): ComponentMeta { + getComponentMeta(componentName: string): IComponentMeta { return this.designer.getComponentMeta( componentName, () => this.simulator?.generateComponentMetadata(componentName) || null, ); } - @obx.ref private _opened = false; - - @obx.ref private _suspensed = false; - - /** - * 是否为非激活状态 - */ - get suspensed(): boolean { - return this._suspensed || !this._opened; - } - - /** - * 与 suspensed 相反,是否为激活状态,这个函数可能用的更多一点 - */ - get active(): boolean { - return !this._suspensed; - } - - /** - * @deprecated 兼容 - */ - get actived(): boolean { - return this.active; - } - - /** - * 是否打开 - */ - get opened() { - return this._opened; - } - /** * 切换激活,只有打开的才能激活 * 不激活,打开之后切换到另外一个时发生,比如 tab 视图,切换到另外一个标签页 @@ -505,22 +670,37 @@ export class DocumentModel { this.rootNode = null; } - checkNesting(dropTarget: ParentalNode, dragObject: DragNodeObject | DragNodeDataObject): boolean { - let items: Array<Node | NodeSchema>; + checkNesting( + dropTarget: INode, + dragObject: IPublicTypeDragNodeObject | IPublicTypeNodeSchema | INode | IPublicTypeDragNodeDataObject, + ): boolean { + let items: Array<INode | IPublicTypeNodeSchema>; if (isDragNodeDataObject(dragObject)) { items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data]; - } else { + } else if (isDragNodeObject<INode>(dragObject)) { items = dragObject.nodes; + } else if (isNode<INode>(dragObject) || isNodeSchema(dragObject)) { + items = [dragObject]; + } else { + console.warn('the dragObject is not in the correct type, dragObject:', dragObject); + return true; } - return items.every((item) => this.checkNestingDown(dropTarget, item)); + return items.every((item) => this.checkNestingDown(dropTarget, item) && this.checkNestingUp(dropTarget, item)); } - checkDropTarget(dropTarget: ParentalNode, dragObject: DragNodeObject | DragNodeDataObject): boolean { - let items: Array<Node | NodeSchema>; + /** + * @deprecated since version 1.0.16. + * Will be deleted in version 2.0.0. + * Use checkNesting method instead. + */ + checkDropTarget(dropTarget: INode, dragObject: IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject): boolean { + let items: Array<INode | IPublicTypeNodeSchema>; if (isDragNodeDataObject(dragObject)) { items = Array.isArray(dragObject.data) ? dragObject.data : [dragObject.data]; - } else { + } else if (isDragNodeObject<INode>(dragObject)) { items = dragObject.nodes; + } else { + return false; } return items.every((item) => this.checkNestingUp(dropTarget, item)); } @@ -528,7 +708,7 @@ export class DocumentModel { /** * 检查对象对父级的要求,涉及配置 parentWhitelist */ - checkNestingUp(parent: ParentalNode, obj: NodeSchema | Node): boolean { + checkNestingUp(parent: INode, obj: IPublicTypeNodeSchema | INode): boolean { if (isNode(obj) || isNodeSchema(obj)) { const config = isNode(obj) ? obj.componentMeta : this.getComponentMeta(obj.componentName); if (config) { @@ -542,9 +722,9 @@ export class DocumentModel { /** * 检查投放位置对子级的要求,涉及配置 childWhitelist */ - checkNestingDown(parent: ParentalNode, obj: NodeSchema | Node): boolean { + checkNestingDown(parent: INode, obj: IPublicTypeNodeSchema | INode): boolean { const config = parent.componentMeta; - return config.checkNestingDown(parent, obj) && this.checkNestingUp(parent, obj); + return config.checkNestingDown(parent, obj); } // ======= compatibles for vision @@ -554,7 +734,7 @@ export class DocumentModel { // add toData toData(extraComps?: string[]) { - const node = this.export(TransformStage.Save); + const node = this.export(IPublicEnumTransformStage.Save); const data = { componentsMap: this.getComponentsMap(extraComps), utils: this.getUtilsMap(), @@ -563,14 +743,10 @@ export class DocumentModel { return data; } - getHistory(): History { + getHistory(): IHistory { return this.history; } - get root() { - return this.rootNode; - } - /** * @deprecated */ @@ -587,7 +763,9 @@ export class DocumentModel { */ /* istanbul ignore next */ exportAddonData() { - const addons = {}; + const addons: { + [key: string]: any; + } = {}; this._addons.forEach((addon) => { const data = addon.exportData(); if (data === null) { @@ -620,7 +798,7 @@ export class DocumentModel { /* istanbul ignore next */ acceptRootNodeVisitor( visitorName = 'default', - visitorFn: (node: RootNode) => any, + visitorFn: (node: IRootNode) => any, ) { let visitorResult = {}; if (!visitorName) { @@ -628,8 +806,10 @@ export class DocumentModel { console.warn('Invalid or empty RootNodeVisitor name.'); } try { - visitorResult = visitorFn.call(this, this.rootNode); - this.rootNodeVisitorMap[visitorName] = visitorResult; + if (this.rootNode) { + visitorResult = visitorFn.call(this, this.rootNode); + this.rootNodeVisitorMap[visitorName] = visitorResult; + } } catch (e) { console.error('RootNodeVisitor is not valid.'); console.error(e); @@ -643,7 +823,7 @@ export class DocumentModel { } getComponentsMap(extraComps?: string[]) { - const componentsMap: ComponentsMap = []; + const componentsMap: IPublicTypeComponentsMap = []; // 组件去重 const exsitingMap: { [componentName: string]: boolean } = {}; for (const node of this._nodesMap.values()) { @@ -666,13 +846,18 @@ export class DocumentModel { } // 合并外界传入的自定义渲染的组件 if (Array.isArray(extraComps)) { - extraComps.forEach(c => { - if (c && !exsitingMap[c]) { - const m = this.getComponentMeta(c); - if (m && m.npm?.package) { + extraComps.forEach((componentName) => { + if (componentName && !exsitingMap[componentName]) { + const meta = this.getComponentMeta(componentName); + if (meta?.npm?.package) { + componentsMap.push({ + ...meta?.npm, + componentName, + }); + } else { componentsMap.push({ - ...m?.npm, - componentName: c, + devMode: 'lowCode', + componentName, }); } } @@ -694,7 +879,7 @@ export class DocumentModel { })); } - onNodeCreate(func: (node: Node) => void) { + onNodeCreate(func: (node: INode) => void) { const wrappedFunc = wrapWithEventSwitch(func); this.emitter.on('nodecreate', wrappedFunc); return () => { @@ -702,7 +887,7 @@ export class DocumentModel { }; } - onNodeDestroy(func: (node: Node) => void) { + onNodeDestroy(func: (node: INode) => void) { const wrappedFunc = wrapWithEventSwitch(func); this.emitter.on('nodedestroy', wrappedFunc); return () => { @@ -724,10 +909,10 @@ export class DocumentModel { console.warn('onRefresh method is deprecated'); } - onReady(fn: Function) { - this.designer.editor.on('document-open', fn); + onReady(fn: (...args: any[]) => void) { + this.designer.editor.eventBus.on('document-open', fn); return () => { - this.designer.editor.removeListener('document-open', fn); + this.designer.editor.eventBus.off('document-open', fn); }; } @@ -736,10 +921,10 @@ export class DocumentModel { } } -export function isDocumentModel(obj: any): obj is DocumentModel { +export function isDocumentModel(obj: any): obj is IDocumentModel { return obj && obj.rootNode; } -export function isPageSchema(obj: any): obj is PageSchema { +export function isPageSchema(obj: any): obj is IPublicTypePageSchema { return obj?.componentName === 'Page'; } diff --git a/packages/designer/src/document/document-view.tsx b/packages/designer/src/document/document-view.tsx index a9641069a7..c6dbe76a81 100644 --- a/packages/designer/src/document/document-view.tsx +++ b/packages/designer/src/document/document-view.tsx @@ -1,11 +1,11 @@ import { Component } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import { DocumentModel } from './document-model'; +import { DocumentModel, IDocumentModel } from './document-model'; import { BuiltinSimulatorHostView } from '../builtin-simulator'; @observer -export class DocumentView extends Component<{ document: DocumentModel }> { +export class DocumentView extends Component<{ document: IDocumentModel }> { render() { const { document } = this.props; const { simulatorProps } = document; @@ -26,7 +26,7 @@ export class DocumentView extends Component<{ document: DocumentModel }> { } } -class DocumentInfoView extends Component<{ document: DocumentModel }> { +class DocumentInfoView extends Component<{ document: IDocumentModel }> { render() { return null; } diff --git a/packages/designer/src/document/history.ts b/packages/designer/src/document/history.ts index 5ac0d99cc3..ca288c03a8 100644 --- a/packages/designer/src/document/history.ts +++ b/packages/designer/src/document/history.ts @@ -1,21 +1,27 @@ -import { EventEmitter } from 'events'; -import { autorun, reaction, mobx, untracked, globalContext, Editor } from '@alilc/lowcode-editor-core'; -import { NodeSchema } from '@alilc/lowcode-types'; -import { History as ShellHistory } from '@alilc/lowcode-shell'; +import { reaction, untracked, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { IPublicTypeNodeSchema, IPublicModelHistory, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { Logger } from '@alilc/lowcode-utils'; +import { IDocumentModel } from '../designer'; -export interface Serialization<K = NodeSchema, T = string> { +const logger = new Logger({ level: 'warn', bizName: 'history' }); + +export interface Serialization<K = IPublicTypeNodeSchema, T = string> { serialize(data: K): T; unserialize(data: T): K; } -export class History<T = NodeSchema> { +export interface IHistory extends IPublicModelHistory { + onStateChange(func: () => any): IPublicTypeDisposable; +} + +export class History<T = IPublicTypeNodeSchema> implements IHistory { private session: Session; private records: Session[]; private point = 0; - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('History'); private asleep = false; @@ -28,41 +34,52 @@ export class History<T = NodeSchema> { }, }; - setSerialization(serialization: Serialization<T, string>) { - this.currentSerialization = serialization; + get hotData() { + return this.session.data; } - constructor(dataFn: () => T, private redoer: (data: T) => void, private timeGap: number = 1000) { + private timeGap: number = 1000; + + constructor( + dataFn: () => T | null, + private redoer: (data: T) => void, + private document?: IDocumentModel, + ) { this.session = new Session(0, null, this.timeGap); this.records = [this.session]; - reaction(() => { + reaction((): any => { return dataFn(); }, (data: T) => { if (this.asleep) return; untracked(() => { const log = this.currentSerialization.serialize(data); - if (this.session.isActive()) { - this.session.log(log); - } else { - this.session.end(); - const lastState = this.getState(); - const cursor = this.session.cursor + 1; - const session = new Session(cursor, log, this.timeGap); - this.session = session; - this.records.splice(cursor, this.records.length - cursor, session); - const currentState = this.getState(); - if (currentState !== lastState) { - this.emitter.emit('statechange', currentState); - } + + // do not record unchanged data + if (this.session.data === log) { + return; + } + + if (this.session.isActive()) { + this.session.log(log); + } else { + this.session.end(); + const lastState = this.getState(); + const cursor = this.session.cursor + 1; + const session = new Session(cursor, log, this.timeGap); + this.session = session; + this.records.splice(cursor, this.records.length - cursor, session); + const currentState = this.getState(); + if (currentState !== lastState) { + this.emitter.emit('statechange', currentState); } - // } + } }); }, { fireImmediately: true }); } - get hotData() { - return this.session.data; + setSerialization(serialization: Serialization<T, string>) { + this.currentSerialization = serialization; } isSavePoint(): boolean { @@ -77,16 +94,18 @@ export class History<T = NodeSchema> { this.asleep = false; } - go(cursor: number) { + go(originalCursor: number) { this.session.end(); - const currentCursor = this.session.cursor; + let cursor = originalCursor; cursor = +cursor; if (cursor < 0) { cursor = 0; } else if (cursor >= this.records.length) { cursor = this.records.length - 1; } + + const currentCursor = this.session.cursor; if (cursor === currentCursor) { return; } @@ -99,7 +118,7 @@ export class History<T = NodeSchema> { this.redoer(this.currentSerialization.unserialize(hotData)); this.emitter.emit('cursor', hotData); } catch (e) /* istanbul ignore next */ { - console.error(e); + logger.error(e); } this.wakeup(); @@ -114,11 +133,11 @@ export class History<T = NodeSchema> { } const cursor = this.session.cursor - 1; this.go(cursor); - const editor = globalContext.get(Editor); + const editor = this.document?.designer.editor; if (!editor) { return; } - editor.emit('history.back', cursor); + editor.eventBus.emit('history.back', cursor); } forward() { @@ -127,11 +146,11 @@ export class History<T = NodeSchema> { } const cursor = this.session.cursor + 1; this.go(cursor); - const editor = globalContext.get(Editor); + const editor = this.document?.designer.editor; if (!editor) { return; } - editor.emit('history.forward', cursor); + editor.eventBus.emit('history.forward', cursor); } savePoint() { @@ -166,14 +185,32 @@ export class History<T = NodeSchema> { return state; } - onStateChange(func: () => any) { + /** + * 监听 state 变更事件 + * @param func + * @returns + */ + onChangeState(func: () => any): IPublicTypeDisposable { + return this.onStateChange(func); + } + + onStateChange(func: () => any): IPublicTypeDisposable { this.emitter.on('statechange', func); return () => { this.emitter.removeListener('statechange', func); }; } - onCursor(func: () => any) { + /** + * 监听历史记录游标位置变更事件 + * @param func + * @returns + */ + onChangeCursor(func: () => any): IPublicTypeDisposable { + return this.onCursor(func); + } + + onCursor(func: () => any): () => void { this.emitter.on('cursor', func); return () => { this.emitter.removeListener('cursor', func); @@ -184,6 +221,7 @@ export class History<T = NodeSchema> { this.emitter.removeAllListeners(); this.records = []; } + /** * * @deprecated @@ -193,10 +231,6 @@ export class History<T = NodeSchema> { isModified() { return this.isSavePoint(); } - - internalToShellHistory() { - return new ShellHistory(this); - } } export class Session { diff --git a/packages/designer/src/document/node/exclusive-group.ts b/packages/designer/src/document/node/exclusive-group.ts index d07cf93c70..9f57b12fc7 100644 --- a/packages/designer/src/document/node/exclusive-group.ts +++ b/packages/designer/src/document/node/exclusive-group.ts @@ -1,17 +1,36 @@ import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core'; import { uniqueId } from '@alilc/lowcode-utils'; -import { TitleContent } from '@alilc/lowcode-types'; -import { Node } from './node'; +import { IPublicTypeTitleContent, IPublicModelExclusiveGroup } from '@alilc/lowcode-types'; +import type { INode } from './node'; import { intl } from '../../locale'; +export interface IExclusiveGroup extends IPublicModelExclusiveGroup<INode> { + readonly name: string; + + get index(): number | undefined; + + remove(node: INode): void; + + add(node: INode): void; + + isVisible(node: INode): boolean; + + get length(): number; + + get visibleNode(): INode; +} + // modals assoc x-hide value, initial: check is Modal, yes will put it in modals, cross levels -// if-else-if assoc conditionGroup value, should be the same level, and siblings, need renderEngine support -export class ExclusiveGroup { +// if-else-if assoc conditionGroup value, should be the same level, +// and siblings, need renderEngine support +export class ExclusiveGroup implements IExclusiveGroup { readonly isExclusiveGroup = true; readonly id = uniqueId('exclusive'); - @obx.shallow readonly children: Node[] = []; + readonly title: IPublicTypeTitleContent; + + @obx.shallow readonly children: INode[] = []; @obx private visibleIndex = 0; @@ -27,11 +46,11 @@ export class ExclusiveGroup { return this.children.length; } - @computed get visibleNode(): Node { + @computed get visibleNode(): INode { return this.children[this.visibleIndex]; } - @computed get firstNode(): Node { + @computed get firstNode(): INode { return this.children[0]!; } @@ -39,8 +58,16 @@ export class ExclusiveGroup { return this.firstNode.index; } - add(node: Node) { - if (node.nextSibling && node.nextSibling.conditionGroup === this) { + constructor(readonly name: string, title?: IPublicTypeTitleContent) { + makeObservable(this); + this.title = title || { + type: 'i18n', + intl: intl('Condition Group'), + }; + } + + add(node: INode) { + if (node.nextSibling && node.nextSibling.conditionGroup?.id === this.id) { const i = this.children.indexOf(node.nextSibling); this.children.splice(i, 0, node); } else { @@ -48,7 +75,7 @@ export class ExclusiveGroup { } } - remove(node: Node) { + remove(node: INode) { const i = this.children.indexOf(node); if (i > -1) { this.children.splice(i, 1); @@ -60,27 +87,17 @@ export class ExclusiveGroup { } } - setVisible(node: Node) { + setVisible(node: INode) { const i = this.children.indexOf(node); if (i > -1) { this.visibleIndex = i; } } - isVisible(node: Node) { + isVisible(node: INode) { const i = this.children.indexOf(node); return i === this.visibleIndex; } - - readonly title: TitleContent; - - constructor(readonly name: string, title?: TitleContent) { - makeObservable(this); - this.title = title || { - type: 'i18n', - intl: intl('Condition Group'), - }; - } } export function isExclusiveGroup(obj: any): obj is ExclusiveGroup { diff --git a/packages/designer/src/document/node/modal-nodes-manager.ts b/packages/designer/src/document/node/modal-nodes-manager.ts index 7f1a7baf29..21c31ab46a 100644 --- a/packages/designer/src/document/node/modal-nodes-manager.ts +++ b/packages/designer/src/document/node/modal-nodes-manager.ts @@ -1,14 +1,15 @@ -import { EventEmitter } from 'events'; -import { Node } from './node'; +import { INode } from './node'; import { DocumentModel } from '../document-model'; +import { IPublicModelModalNodesManager } from '@alilc/lowcode-types'; +import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; -export function getModalNodes(node: Node) { +export function getModalNodes(node: INode) { if (!node) return []; let nodes: any = []; if (node.componentMeta.isModal) { nodes.push(node); } - const children = node.getChildren(); + const { children } = node; if (children) { children.forEach((child) => { nodes = nodes.concat(getModalNodes(child)); @@ -17,20 +18,23 @@ export function getModalNodes(node: Node) { return nodes; } -export class ModalNodesManager { - public willDestroy: any; +export interface IModalNodesManager extends IPublicModelModalNodesManager<INode> { +} + +export class ModalNodesManager implements IModalNodesManager { + willDestroy: any; private page: DocumentModel; - private modalNodes: Node[]; + private modalNodes: INode[]; private nodeRemoveEvents: any; - private emitter: EventEmitter; + private emitter: IEventBus; constructor(page: DocumentModel) { this.page = page; - this.emitter = new EventEmitter(); + this.emitter = createModuleEventBus('ModalNodesManager'); this.nodeRemoveEvents = {}; this.setNodes(); this.hideModalNodes(); @@ -40,26 +44,27 @@ export class ModalNodesManager { ]; } - getModalNodes() { + getModalNodes(): INode[] { return this.modalNodes; } - getVisibleModalNode() { - return this.getModalNodes().find((node: Node) => node.getVisible()); + getVisibleModalNode(): INode | null { + const visibleNode = this.getModalNodes().find((node: INode) => node.getVisible()); + return visibleNode || null; } hideModalNodes() { - this.modalNodes.forEach((node: Node) => { + this.modalNodes.forEach((node: INode) => { node.setVisible(false); }); } - setVisible(node: Node) { + setVisible(node: INode) { this.hideModalNodes(); node.setVisible(true); } - setInvisible(node: Node) { + setInvisible(node: INode) { node.setVisible(false); } @@ -77,8 +82,8 @@ export class ModalNodesManager { }; } - private addNode(node: Node) { - if (node.componentMeta.isModal) { + private addNode(node: INode) { + if (node?.componentMeta.isModal) { this.hideModalNodes(); this.modalNodes.push(node); this.addNodeEvent(node); @@ -87,7 +92,7 @@ export class ModalNodesManager { } } - private removeNode(node: Node) { + private removeNode(node: INode) { if (node.componentMeta.isModal) { const index = this.modalNodes.indexOf(node); if (index >= 0) { @@ -101,24 +106,24 @@ export class ModalNodesManager { } } - private addNodeEvent(node: Node) { - this.nodeRemoveEvents[node.getId()] = + private addNodeEvent(node: INode) { + this.nodeRemoveEvents[node.id] = node.onVisibleChange(() => { this.emitter.emit('visibleChange'); }); } - private removeNodeEvent(node: Node) { - if (this.nodeRemoveEvents[node.getId()]) { - this.nodeRemoveEvents[node.getId()](); - delete this.nodeRemoveEvents[node.getId()]; + private removeNodeEvent(node: INode) { + if (this.nodeRemoveEvents[node.id]) { + this.nodeRemoveEvents[node.id](); + delete this.nodeRemoveEvents[node.id]; } } setNodes() { - const nodes = getModalNodes(this.page.getRoot()!); + const nodes = getModalNodes(this.page.rootNode!); this.modalNodes = nodes; - this.modalNodes.forEach((node: Node) => { + this.modalNodes.forEach((node: INode) => { this.addNodeEvent(node); }); diff --git a/packages/designer/src/document/node/node-children.ts b/packages/designer/src/document/node/node-children.ts index 728df7cb8a..65210fe62c 100644 --- a/packages/designer/src/document/node/node-children.ts +++ b/packages/designer/src/document/node/node-children.ts @@ -1,9 +1,7 @@ -import { obx, computed, globalContext, makeObservable } from '@alilc/lowcode-editor-core'; -import { Node, ParentalNode } from './node'; -import { TransformStage } from './transform-stage'; -import { NodeData, isNodeSchema } from '@alilc/lowcode-types'; -import { shallowEqual, compatStage } from '@alilc/lowcode-utils'; -import { EventEmitter } from 'events'; +import { obx, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { Node, INode } from './node'; +import { IPublicTypeNodeData, IPublicModelNodeChildren, IPublicEnumTransformStage, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { shallowEqual, compatStage, isNodeSchema } from '@alilc/lowcode-utils'; import { foreachReverse } from '../../utils/tree'; import { NodeRemoveOptions } from '../../types'; @@ -12,15 +10,100 @@ export interface IOnChangeOptions { node: Node; } -export class NodeChildren { - @obx.shallow private children: Node[]; +export interface INodeChildren extends Omit<IPublicModelNodeChildren<INode>, + 'importSchema' | + 'exportSchema' | + 'isEmpty' | + 'notEmpty' +> { + children: INode[]; - private emitter = new EventEmitter(); + get owner(): INode; - constructor(readonly owner: ParentalNode, data: NodeData | NodeData[], options: any = {}) { + get length(): number; + + unlinkChild(node: INode): void; + + /** + * 删除一个节点 + */ + internalDelete( + node: INode, + purge: boolean, + useMutator: boolean, + options: NodeRemoveOptions + ): boolean; + + /** + * 插入一个节点,返回新长度 + */ + internalInsert(node: INode, at?: number | null, useMutator?: boolean): void; + + import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId?: boolean): void; + + /** + * 导出 schema + */ + export(stage: IPublicEnumTransformStage): IPublicTypeNodeData[]; + + /** following methods are overriding super interface, using different param types */ + /** overriding methods start */ + + forEach(fn: (item: INode, index: number) => void): void; + + /** + * 根据索引获得节点 + */ + get(index: number): INode | null; + + isEmpty(): boolean; + + notEmpty(): boolean; + + internalInitParent(): void; + + onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable; + + /** overriding methods end */ +} +export class NodeChildren implements INodeChildren { + @obx.shallow children: INode[]; + + private emitter: IEventBus = createModuleEventBus('NodeChildren'); + + /** + * 元素个数 + */ + @computed get size(): number { + return this.children.length; + } + + get isEmptyNode(): boolean { + return this.size < 1; + } + get notEmptyNode(): boolean { + return this.size > 0; + } + + @computed get length(): number { + return this.children.length; + } + + private purged = false; + + get [Symbol.toStringTag]() { + // 保证向前兼容性 + return 'Array'; + } + + constructor( + readonly owner: INode, + data: IPublicTypeNodeData | IPublicTypeNodeData[], + options: any = {}, + ) { makeObservable(this); - this.children = (Array.isArray(data) ? data : [data]).map((child) => { - return this.owner.document.createNode(child, options.checkId); + this.children = (Array.isArray(data) ? data : [data]).filter(child => !!child).map((child) => { + return this.owner.document?.createNode(child, options.checkId); }); } @@ -31,20 +114,20 @@ export class NodeChildren { /** * 导出 schema */ - export(stage: TransformStage = TransformStage.Save): NodeData[] { + export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeNodeData[] { stage = compatStage(stage); return this.children.map((node) => { const data = node.export(stage); - if (node.isLeaf() && TransformStage.Save === stage) { + if (node.isLeafNode && IPublicEnumTransformStage.Save === stage) { // FIXME: filter empty - return data.children as NodeData; + return data.children as IPublicTypeNodeData; } return data; }); } - import(data?: NodeData | NodeData[], checkId = false) { - data = data ? (Array.isArray(data) ? data : [data]) : []; + import(data?: IPublicTypeNodeData | IPublicTypeNodeData[], checkId = false) { + data = (data ? (Array.isArray(data) ? data : [data]) : []).filter(d => !!d); const originChildren = this.children.slice(); this.children.forEach((child) => child.internalSetParent(null)); @@ -54,12 +137,12 @@ export class NodeChildren { const child = originChildren[i]; const item = data[i]; - let node: Node | undefined; + let node: INode | undefined | null; if (isNodeSchema(item) && !checkId && child && child.componentName === item.componentName) { node = child; node.import(item); } else { - node = this.owner.document.createNode(item, checkId); + node = this.owner.document?.createNode(item, checkId); } children[i] = node; } @@ -75,34 +158,21 @@ export class NodeChildren { * @deprecated * @param nodes */ - concat(nodes: Node[]) { + concat(nodes: INode[]) { return this.children.concat(nodes); } /** - * 元素个数 - */ - @computed get size(): number { - return this.children.length; - } - - /** - * 是否空 + * */ isEmpty() { - return this.size < 1; + return this.isEmptyNode; } notEmpty() { - return this.size > 0; - } - - @computed get length(): number { - return this.children.length; + return this.notEmptyNode; } - private purged = false; - /** * 回收销毁 */ @@ -116,8 +186,8 @@ export class NodeChildren { }); } - unlinkChild(node: Node) { - const i = this.children.indexOf(node); + unlinkChild(node: INode) { + const i = this.children.map(d => d.id).indexOf(node.id); if (i < 0) { return false; } @@ -131,9 +201,16 @@ export class NodeChildren { /** * 删除一个节点 */ - delete(node: Node, purge = false, useMutator = true, options: NodeRemoveOptions = {}): boolean { + delete(node: INode): boolean { + return this.internalDelete(node); + } + + /** + * 删除一个节点 + */ + internalDelete(node: INode, purge = false, useMutator = true, options: NodeRemoveOptions = {}): boolean { node.internalPurgeStart(); - if (node.isParental()) { + if (node.isParentalNode) { foreachReverse( node.children, (subNode: Node) => { @@ -149,8 +226,8 @@ export class NodeChildren { (iterable, idx) => (iterable as [])[idx], ); } - // 需要在从 children 中删除 node 前记录下 index,internalSetParent 中会执行删除(unlink)操作 - const i = this.children.indexOf(node); + // 需要在从 children 中删除 node 前记录下 index,internalSetParent 中会执行删除 (unlink) 操作 + const i = this.children.map(d => d.id).indexOf(node.id); if (purge) { // should set parent null node.internalSetParent(null, useMutator); @@ -162,12 +239,11 @@ export class NodeChildren { } const { document } = node; /* istanbul ignore next */ - if (globalContext.has('editor')) { - globalContext.get('editor').emit('node.remove', { node, index: i }); - } - document.unlinkNode(node); - document.selection.remove(node.id); - document.destroyNode(node); + const editor = node.document?.designer.editor; + editor?.eventBus.emit('node.remove', { node, index: i }); + document?.unlinkNode(node); + document?.selection.remove(node.id); + document?.destroyNode(node); this.emitter.emit('change', { type: 'delete', node, @@ -188,22 +264,25 @@ export class NodeChildren { return false; } + insert(node: INode, at?: number | null): void { + this.internalInsert(node, at, true); + } + /** * 插入一个节点,返回新长度 */ - insert(node: Node, at?: number | null, useMutator = true): void { + internalInsert(node: INode, at?: number | null, useMutator = true): void { const { children } = this; let index = at == null || at === -1 ? children.length : at; - const i = children.indexOf(node); + const i = children.map(d => d.id).indexOf(node.id); if (node.parent) { - /* istanbul ignore next */ - globalContext.has('editor') && - globalContext.get('editor').emit('node.remove.topLevel', { - node, - index: node.index, - }); + const editor = node.document?.designer.editor; + editor?.eventBus.emit('node.remove.topLevel', { + node, + index: node.index, + }); } if (i < 0) { @@ -232,9 +311,8 @@ export class NodeChildren { }); this.emitter.emit('insert', node); /* istanbul ignore next */ - if (globalContext.has('editor')) { - globalContext.get('editor').emit('node.add', { node }); - } + const editor = node.document?.designer.editor; + editor?.eventBus.emit('node.add', { node }); if (useMutator) { this.reportModified(node, this.owner, { type: 'insert' }); } @@ -265,14 +343,14 @@ export class NodeChildren { /** * 取得节点索引编号 */ - indexOf(node: Node): number { - return this.children.indexOf(node); + indexOf(node: INode): number { + return this.children.map(d => d.id).indexOf(node.id); } /** * */ - splice(start: number, deleteCount: number, node?: Node): Node[] { + splice(start: number, deleteCount: number, node?: INode): INode[] { if (node) { return this.children.splice(start, deleteCount, node); } @@ -282,21 +360,21 @@ export class NodeChildren { /** * 根据索引获得节点 */ - get(index: number): Node | null { + get(index: number): INode | null { return this.children.length > index ? this.children[index] : null; } /** * 是否存在节点 */ - has(node: Node) { + has(node: INode) { return this.indexOf(node) > -1; } /** * 迭代器 */ - [Symbol.iterator](): { next(): { value: Node } } { + [Symbol.iterator](): { next(): { value: INode } } { let index = 0; const { children } = this; const length = children.length || 0; @@ -319,7 +397,7 @@ export class NodeChildren { /** * 遍历 */ - forEach(fn: (item: Node, index: number) => void): void { + forEach(fn: (item: INode, index: number) => void): void { this.children.forEach((child, index) => { return fn(child, index); }); @@ -328,43 +406,47 @@ export class NodeChildren { /** * 遍历 */ - map<T>(fn: (item: Node, index: number) => T): T[] | null { + map<T>(fn: (item: INode, index: number) => T): T[] | null { return this.children.map((child, index) => { return fn(child, index); }); } - every(fn: (item: Node, index: number) => any): boolean { + every(fn: (item: INode, index: number) => any): boolean { return this.children.every((child, index) => fn(child, index)); } - some(fn: (item: Node, index: number) => any): boolean { + some(fn: (item: INode, index: number) => any): boolean { return this.children.some((child, index) => fn(child, index)); } - filter(fn: (item: Node, index: number) => any) { + filter(fn: (item: INode, index: number) => any): any { return this.children.filter(fn); } - find(fn: (item: Node, index: number) => boolean) { + find(fn: (item: INode, index: number) => boolean): INode | undefined { return this.children.find(fn); } - reduce(fn: (acc: any, cur: Node) => any, initialValue: any) { + reduce(fn: (acc: any, cur: INode) => any, initialValue: any): void { return this.children.reduce(fn, initialValue); } + reverse() { + return this.children.reverse(); + } + mergeChildren( - remover: (node: Node, idx: number) => boolean, - adder: (children: Node[]) => NodeData[] | null, - sorter: (firstNode: Node, secondNode: Node) => number, - ) { + remover: (node: INode, idx: number) => boolean, + adder: (children: INode[]) => IPublicTypeNodeData[] | null, + sorter: (firstNode: INode, secondNode: INode) => number, + ): any { let changed = false; if (remover) { const willRemove = this.children.filter(remover); if (willRemove.length > 0) { willRemove.forEach((node) => { - const i = this.children.indexOf(node); + const i = this.children.map(d => d.id).indexOf(node.id); if (i > -1) { this.children.splice(i, 1); node.remove(false); @@ -376,10 +458,13 @@ export class NodeChildren { if (adder) { const items = adder(this.children); if (items && items.length > 0) { - items.forEach((child: NodeData) => { - const node = this.owner.document.createNode(child); + items.forEach((child: IPublicTypeNodeData) => { + const node: INode = this.owner.document?.createNode(child); this.children.push(node); node.internalSetParent(this.owner); + /* istanbul ignore next */ + const editor = node.document?.designer.editor; + editor?.eventBus.emit('node.add', { node }); }); changed = true; } @@ -393,33 +478,28 @@ export class NodeChildren { } } - onChange(fn: (info?: IOnChangeOptions) => void): () => void { + onChange(fn: (info?: IOnChangeOptions) => void): IPublicTypeDisposable { this.emitter.on('change', fn); return () => { this.emitter.removeListener('change', fn); }; } - onInsert(fn: (node: Node) => void) { + onInsert(fn: (node: INode) => void) { this.emitter.on('insert', fn); return () => { this.emitter.removeListener('insert', fn); }; } - get [Symbol.toStringTag]() { - // 保证向前兼容性 - return 'Array'; - } - - private reportModified(node: Node, owner: Node, options = {}) { + private reportModified(node: INode, owner: INode, options = {}) { if (!node) { return; } - if (node.isRoot()) { + if (node.isRootNode) { return; } - const callbacks = owner.componentMeta.getMetadata().configure.advanced?.callbacks; + const callbacks = owner.componentMeta?.advanced.callbacks; if (callbacks?.onSubtreeModified) { try { callbacks?.onSubtreeModified.call( @@ -432,7 +512,7 @@ export class NodeChildren { } } - if (owner.parent && !owner.parent.isRoot()) { + if (owner.parent && !owner.parent.isRootNode) { this.reportModified(node, owner.parent, { ...options, propagated: true }); } } diff --git a/packages/designer/src/document/node/node.ts b/packages/designer/src/document/node/node.ts index 9fe98e4734..c8363d0586 100644 --- a/packages/designer/src/document/node/node.ts +++ b/packages/designer/src/document/node/node.ts @@ -1,35 +1,168 @@ import { ReactElement } from 'react'; -import { EventEmitter } from 'events'; -import { obx, computed, autorun, makeObservable, runInAction, wrapWithEventSwitch, action } from '@alilc/lowcode-editor-core'; +import { obx, computed, autorun, makeObservable, runInAction, wrapWithEventSwitch, action, createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; import { - isDOMText, - isJSExpression, - NodeSchema, - PropsMap, - PropsList, - NodeData, - I18nData, - SlotSchema, - PageSchema, - ComponentSchema, - NodeStatus, - CompositeValue, + IPublicTypeNodeSchema, + IPublicTypePropsMap, + IPublicTypePropsList, + IPublicTypeNodeData, + IPublicTypeI18nData, + IPublicTypeSlotSchema, + IPublicTypePageSchema, + IPublicTypeComponentSchema, + IPublicTypeCompositeValue, GlobalEvent, - ComponentAction, + IPublicTypeComponentAction, + IPublicModelNode, + IPublicModelExclusiveGroup, + IPublicEnumTransformStage, + IPublicTypeDisposable, + IBaseModelNode, } from '@alilc/lowcode-types'; -import { compatStage } from '@alilc/lowcode-utils'; -import { SettingTopEntry } from '@alilc/lowcode-designer'; -import { Node as ShellNode } from '@alilc/lowcode-shell'; -import { Props, getConvertedExtraKey } from './props/props'; -import { DocumentModel } from '../document-model'; -import { NodeChildren } from './node-children'; -import { Prop } from './props/prop'; -import { ComponentMeta } from '../../component-meta'; +import { compatStage, isDOMText, isJSExpression, isNode, isNodeSchema } from '@alilc/lowcode-utils'; +import { ISettingTopEntry } from '@alilc/lowcode-designer'; +import { Props, getConvertedExtraKey, IProps } from './props/props'; +import type { IDocumentModel } from '../document-model'; +import { NodeChildren, INodeChildren } from './node-children'; +import { IProp, Prop } from './props/prop'; +import type { IComponentMeta } from '../../component-meta'; import { ExclusiveGroup, isExclusiveGroup } from './exclusive-group'; -import { TransformStage } from './transform-stage'; +import type { IExclusiveGroup } from './exclusive-group'; import { includeSlot, removeSlot } from '../../utils/slot'; import { foreachReverse } from '../../utils/tree'; -import { NodeRemoveOptions } from '../../types'; +import { NodeRemoveOptions, EDITOR_EVENT } from '../../types'; + +export interface NodeStatus { + locking: boolean; + pseudo: boolean; + inPlaceEditing: boolean; +} + +export interface IBaseNode<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema> extends Omit<IBaseModelNode< + IDocumentModel, + IBaseNode, + INodeChildren, + IComponentMeta, + ISettingTopEntry, + IProps, + IProp, + IExclusiveGroup +>, + 'isRoot' | + 'isPage' | + 'isComponent' | + 'isModal' | + 'isSlot' | + 'isParental' | + 'isLeaf' | + 'settingEntry' | + // 在内部的 node 模型中不存在 + 'getExtraPropValue' | + 'setExtraPropValue' | + 'exportSchema' | + 'visible' | + 'importSchema' | + // 内外实现有差异 + 'isContainer' | + 'isEmpty' +> { + isNode: boolean; + + get componentMeta(): IComponentMeta; + + get settingEntry(): ISettingTopEntry; + + get isPurged(): boolean; + + get index(): number | undefined; + + get isPurging(): boolean; + + getId(): string; + + getParent(): INode | null; + + /** + * 内部方法,请勿使用 + * @param useMutator 是否触发联动逻辑 + */ + internalSetParent(parent: INode | null, useMutator?: boolean): void; + + setConditionGroup(grp: IPublicModelExclusiveGroup | string | null): void; + + internalToShellNode(): IPublicModelNode | null; + + internalPurgeStart(): void; + + unlinkSlot(slotNode: INode): void; + + /** + * 导出 schema + */ + export<T = Schema>(stage: IPublicEnumTransformStage, options?: any): T; + + emitPropChange(val: IPublicTypePropChangeOptions): void; + + import(data: Schema, checkId?: boolean): void; + + internalSetSlotFor(slotFor: Prop | null | undefined): void; + + addSlot(slotNode: INode): void; + + onVisibleChange(func: (flag: boolean) => any): () => void; + + getSuitablePlace(node: INode, ref: any): any; + + onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined; + + onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable; + + isModal(): boolean; + + isRoot(): boolean; + + isPage(): boolean; + + isComponent(): boolean; + + isSlot(): boolean; + + isParental(): boolean; + + isLeaf(): boolean; + + isContainer(): boolean; + + isEmpty(): boolean; + + remove( + useMutator?: boolean, + purge?: boolean, + options?: NodeRemoveOptions, + ): void; + + didDropIn(dragment: INode): void; + + didDropOut(dragment: INode): void; + + purge(): void; + + removeSlot(slotNode: INode): boolean; + + setVisible(flag: boolean): void; + + getVisible(): boolean; + + getChildren(): INodeChildren | null; + + clearPropValue(path: string | number): void; + + setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null): void; + + mergeProps(props: IPublicTypePropsMap): void; + + /** 是否可以选中 */ + canSelect(): boolean; +} /** * 基础节点 @@ -79,8 +212,8 @@ import { NodeRemoveOptions } from '../../types'; * isLocked * hidden */ -export class Node<Schema extends NodeSchema = NodeSchema> { - private emitter: EventEmitter; +export class Node<Schema extends IPublicTypeNodeSchema = IPublicTypeNodeSchema> implements IBaseNode { + private emitter: IEventBus; /** * 是节点实例 @@ -94,7 +227,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 节点组件类型 - * 特殊节点: + * 特殊节点: * * Page 页面 * * Block 区块 * * Component 组件/元件 @@ -107,28 +240,28 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 属性抽象 */ - props: Props; + props: IProps; - protected _children?: NodeChildren; + protected _children?: INodeChildren; /** * @deprecated */ private _addons: { [key: string]: { exportData: () => any; isProp: boolean } } = {}; - @obx.ref private _parent: ParentalNode | null = null; + @obx.ref private _parent: INode | null = null; /** * 父级节点 */ - get parent(): ParentalNode | null { + get parent(): INode | null { return this._parent; } /** * 当前节点子集 */ - get children(): NodeChildren | null { + get children(): INodeChildren | null { return this._children || null; } @@ -142,7 +275,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return 0; } - @computed get title(): string | I18nData | ReactElement { + @computed get title(): string | IPublicTypeI18nData | ReactElement { let t = this.getExtraProp('title'); // TODO: 暂时走不到这个分支 // if (!t && this.componentMeta.descriptor) { @@ -163,7 +296,79 @@ export class Node<Schema extends NodeSchema = NodeSchema> { isInited = false; - constructor(readonly document: DocumentModel, nodeSchema: Schema, options: any = {}) { + _settingEntry: ISettingTopEntry; + + get settingEntry(): ISettingTopEntry { + if (this._settingEntry) return this._settingEntry; + this._settingEntry = this.document.designer.createSettingEntry([this]); + return this._settingEntry; + } + + private autoruns?: Array<() => void>; + + private _isRGLContainer = false; + + set isRGLContainer(status: boolean) { + this._isRGLContainer = status; + } + + get isRGLContainer(): boolean { + return !!this._isRGLContainer; + } + + set isRGLContainerNode(status: boolean) { + this._isRGLContainer = status; + } + + get isRGLContainerNode(): boolean { + return !!this._isRGLContainer; + } + + get isEmptyNode() { + return this.isEmpty(); + } + + private _slotFor?: IProp | null | undefined = null; + + @obx.shallow _slots: INode[] = []; + + get slots(): INode[] { + return this._slots; + } + + /* istanbul ignore next */ + @obx.ref private _conditionGroup: IExclusiveGroup | null = null; + + /* istanbul ignore next */ + get conditionGroup(): IExclusiveGroup | null { + return this._conditionGroup; + } + + private purged = false; + + /** + * 是否已销毁 + */ + get isPurged() { + return this.purged; + } + + private purging: boolean = false; + + /** + * 是否正在销毁 + */ + get isPurging() { + return this.purging; + } + + @obx.shallow status: NodeStatus = { + inPlaceEditing: false, + locking: false, + pseudo: false, + }; + + constructor(readonly document: IDocumentModel, nodeSchema: Schema) { makeObservable(this); const { componentName, id, children, props, ...extras } = nodeSchema; this.id = document.nextId(id); @@ -174,7 +379,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { }); } else { this.props = new Props(this, props, extras); - this._children = new NodeChildren(this as ParentalNode, this.initialChildren(children)); + this._children = new NodeChildren(this as INode, this.initialChildren(children)); this._children.internalInitParent(); this.props.merge( this.upgradeProps(this.initProps(props || {})), @@ -186,15 +391,17 @@ export class Node<Schema extends NodeSchema = NodeSchema> { this.initBuiltinProps(); this.isInited = true; - this.emitter = new EventEmitter(); - } - - _settingEntry: SettingTopEntry; - - get settingEntry(): SettingTopEntry { - if (this._settingEntry) return this._settingEntry; - this._settingEntry = this.document.designer.createSettingEntry([this]); - return this._settingEntry; + this.emitter = createModuleEventBus('Node'); + const { editor } = this.document.designer; + this.onVisibleChange((visible: boolean) => { + editor?.eventBus.emit(EDITOR_EVENT.NODE_VISIBLE_CHANGE, this, visible); + }); + this.onChildrenChange((info?: { type: string; node: INode }) => { + editor?.eventBus.emit(EDITOR_EVENT.NODE_CHILDREN_CHANGE, { + type: info?.type, + node: this, + }); + }); } /** @@ -212,87 +419,112 @@ export class Node<Schema extends NodeSchema = NodeSchema> { @action private initProps(props: any): any { - return this.document.designer.transformProps(props, this, TransformStage.Init); + return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Init); } @action private upgradeProps(props: any): any { - return this.document.designer.transformProps(props, this, TransformStage.Upgrade); + return this.document.designer.transformProps(props, this, IPublicEnumTransformStage.Upgrade); } - private autoruns?: Array<() => void>; - private setupAutoruns() { - const autoruns = this.componentMeta.getMetadata().configure.advanced?.autoruns; + const { autoruns } = this.componentMeta.advanced; if (!autoruns || autoruns.length < 1) { return; } this.autoruns = autoruns.map((item) => { return autorun(() => { - item.autorun(this.props.get(item.name, true) as any); + item.autorun(this.props.getNode().settingEntry.get(item.name)?.internalToShellField()); }); }); } - private initialChildren(children: any): NodeData[] { - // FIXME! this is dirty code + private initialChildren(children: IPublicTypeNodeData | IPublicTypeNodeData[] | undefined): IPublicTypeNodeData[] { + const { initialChildren } = this.componentMeta.advanced; + if (children == null) { - const initialChildren = this.componentMeta.getMetadata().configure.advanced?.initialChildren; if (initialChildren) { if (typeof initialChildren === 'function') { - return initialChildren(this as any) || []; + return initialChildren(this.internalToShellNode()!) || []; } return initialChildren; } + return []; } - return children || []; - } - private _isRGLContainer = false; + if (Array.isArray(children)) { + return children; + } - set isRGLContainer(status: boolean) { - this._isRGLContainer = status; + return [children]; } - get isRGLContainer(): boolean { - return !!this._isRGLContainer; + isContainer(): boolean { + return this.isContainerNode; } - isContainer(): boolean { - return this.isParental() && this.componentMeta.isContainer; + get isContainerNode(): boolean { + return this.isParentalNode && this.componentMeta.isContainer; } isModal(): boolean { + return this.isModalNode; + } + + get isModalNode(): boolean { return this.componentMeta.isModal; } isRoot(): boolean { + return this.isRootNode; + } + + get isRootNode(): boolean { return this.document.rootNode === (this as any); } isPage(): boolean { - return this.isRoot() && this.componentName === 'Page'; + return this.isPageNode; + } + + get isPageNode(): boolean { + return this.isRootNode && this.componentName === 'Page'; } isComponent(): boolean { - return this.isRoot() && this.componentName === 'Component'; + return this.isComponentNode; + } + + get isComponentNode(): boolean { + return this.isRootNode && this.componentName === 'Component'; } isSlot(): boolean { + return this.isSlotNode; + } + + get isSlotNode(): boolean { return this._slotFor != null && this.componentName === 'Slot'; } /** * 是否一个父亲类节点 */ - isParental(): this is ParentalNode { - return !this.isLeaf(); + isParental(): boolean { + return this.isParentalNode; + } + + get isParentalNode(): boolean { + return !this.isLeafNode; } /** * 终端节点,内容一般为 文字 或者 表达式 */ - isLeaf(): this is LeafNode { + isLeaf(): boolean { + return this.isLeafNode; + } + get isLeafNode(): boolean { return this.componentName === 'Leaf'; } @@ -301,8 +533,8 @@ export class Node<Schema extends NodeSchema = NodeSchema> { this.document.addWillPurge(this); } - private didDropIn(dragment: Node) { - const callbacks = this.componentMeta.getMetadata().configure.advanced?.callbacks; + didDropIn(dragment: INode) { + const { callbacks } = this.componentMeta.advanced; if (callbacks?.onNodeAdd) { const cbThis = this.internalToShellNode(); callbacks?.onNodeAdd.call(cbThis, dragment.internalToShellNode(), cbThis); @@ -312,8 +544,8 @@ export class Node<Schema extends NodeSchema = NodeSchema> { } } - private didDropOut(dragment: Node) { - const callbacks = this.componentMeta.getMetadata().configure.advanced?.callbacks; + didDropOut(dragment: INode) { + const { callbacks } = this.componentMeta.advanced; if (callbacks?.onNodeRemove) { const cbThis = this.internalToShellNode(); callbacks?.onNodeRemove.call(cbThis, dragment.internalToShellNode(), cbThis); @@ -327,7 +559,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { * 内部方法,请勿使用 * @param useMutator 是否触发联动逻辑 */ - internalSetParent(parent: ParentalNode | null, useMutator = false) { + internalSetParent(parent: INode | null, useMutator = false) { if (this._parent === parent) { return; } @@ -337,7 +569,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { if (this.isSlot()) { this._parent.unlinkSlot(this); } else { - this._parent.children.unlinkChild(this); + this._parent.children?.unlinkChild(this); } } if (useMutator) { @@ -362,20 +594,18 @@ export class Node<Schema extends NodeSchema = NodeSchema> { } } - private _slotFor?: Prop | null = null; - internalSetSlotFor(slotFor: Prop | null | undefined) { this._slotFor = slotFor; } - internalToShellNode(): ShellNode | null { - return ShellNode.create(this); + internalToShellNode(): IPublicModelNode | null { + return this.document.designer.shellModelFactory.createNode(this); } /** * 关联属性 */ - get slotFor() { + get slotFor(): IProp | null | undefined { return this._slotFor; } @@ -389,16 +619,16 @@ export class Node<Schema extends NodeSchema = NodeSchema> { ) { if (this.parent) { if (!options.suppressRemoveEvent) { - this.document.designer.editor?.emit('node.remove.topLevel', { + this.document.designer.editor?.eventBus.emit('node.remove.topLevel', { node: this, index: this.parent?.children?.indexOf(this), }); } if (this.isSlot()) { - this.parent.removeSlot(this, purge); - this.parent.children.delete(this, purge, useMutator, { suppressRemoveEvent: true }); + this.parent.removeSlot(this); + this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true }); } else { - this.parent.children.delete(this, purge, useMutator, { suppressRemoveEvent: true }); + this.parent.children?.internalDelete(this, purge, useMutator, { suppressRemoveEvent: true }); } } } @@ -417,6 +647,12 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return !!this.getExtraProp('isLocked')?.getValue(); } + canSelect(): boolean { + const onSelectHook = this.componentMeta?.advanced?.callbacks?.onSelectHook; + const canSelect = typeof onSelectHook === 'function' ? onSelectHook(this.internalToShellNode()!) : true; + return canSelect; + } + /** * 选择当前节点 */ @@ -438,37 +674,24 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 节点组件描述 */ - @computed get componentMeta(): ComponentMeta { + @computed get componentMeta(): IComponentMeta { return this.document.getComponentMeta(this.componentName); } - @computed get propsData(): PropsMap | PropsList | null { + @computed get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null { if (!this.isParental() || this.componentName === 'Fragment') { return null; } - return this.props.export(TransformStage.Serilize).props || null; + return this.props.export(IPublicEnumTransformStage.Serilize).props || null; } - @obx.shallow _slots: Node[] = []; - hasSlots() { return this._slots.length > 0; } - get slots() { - return this._slots; - } - - /* istanbul ignore next */ - @obx.ref private _conditionGroup: ExclusiveGroup | null = null; - - /* istanbul ignore next */ - get conditionGroup(): ExclusiveGroup | null { - return this._conditionGroup; - } - /* istanbul ignore next */ - setConditionGroup(grp: ExclusiveGroup | string | null) { + setConditionGroup(grp: IPublicModelExclusiveGroup | string | null) { + let _grp: IExclusiveGroup | null = null; if (!grp) { this.getExtraProp('conditionGroup', false)?.remove(); if (this._conditionGroup) { @@ -479,20 +702,20 @@ export class Node<Schema extends NodeSchema = NodeSchema> { } if (!isExclusiveGroup(grp)) { if (this.prevSibling?.conditionGroup?.name === grp) { - grp = this.prevSibling.conditionGroup; + _grp = this.prevSibling.conditionGroup; } else if (this.nextSibling?.conditionGroup?.name === grp) { - grp = this.nextSibling.conditionGroup; - } else { - grp = new ExclusiveGroup(grp); + _grp = this.nextSibling.conditionGroup; + } else if (typeof grp === 'string') { + _grp = new ExclusiveGroup(grp); } } - if (this._conditionGroup !== grp) { - this.getExtraProp('conditionGroup', true)?.setValue(grp.name); + if (_grp && this._conditionGroup !== _grp) { + this.getExtraProp('conditionGroup', true)?.setValue(_grp.name); if (this._conditionGroup) { this._conditionGroup.remove(this); } - this._conditionGroup = grp; - grp.add(this); + this._conditionGroup = _grp; + _grp?.add(this); } } @@ -545,16 +768,20 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 替换子节点 * - * @param {Node} node + * @param {INode} node * @param {object} data */ - replaceChild(node: Node, data: any): Node { + replaceChild(node: INode, data: any): INode | null { if (this.children?.has(node)) { const selected = this.document.selection.has(node.id); delete data.id; const newNode = this.document.createNode(data); + if (!isNode(newNode)) { + return null; + } + this.insertBefore(newNode, node, false); node.remove(false); @@ -583,15 +810,15 @@ export class Node<Schema extends NodeSchema = NodeSchema> { }; } - getProp(path: string, createIfNone = true): Prop | null { + getProp(path: string, createIfNone = true): IProp | null { return this.props.query(path, createIfNone) || null; } - getExtraProp(key: string, createIfNone = true): Prop | null { + getExtraProp(key: string, createIfNone = true): IProp | null { return this.props.get(getConvertedExtraKey(key), createIfNone) || null; } - setExtraProp(key: string, value: CompositeValue) { + setExtraProp(key: string, value: IPublicTypeCompositeValue) { this.getProp(getConvertedExtraKey(key), true)?.setValue(value); } @@ -619,14 +846,14 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 设置多个属性值,和原有值合并 */ - mergeProps(props: PropsMap) { + mergeProps(props: IPublicTypePropsMap) { this.props.merge(props); } /** * 设置多个属性值,替换原有值 */ - setProps(props?: PropsMap | PropsList | Props | null) { + setProps(props?: IPublicTypePropsMap | IPublicTypePropsList | Props | null) { if (props instanceof Props) { this.props = props; return; @@ -637,46 +864,52 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 获取节点在父容器中的索引 */ - @computed get index(): number { + @computed get index(): number | undefined { if (!this.parent) { return -1; } - return this.parent.children.indexOf(this); + return this.parent.children?.indexOf(this); } /** * 获取下一个兄弟节点 */ - get nextSibling(): Node | null { + get nextSibling(): INode | null | undefined { if (!this.parent) { return null; } const { index } = this; + if (typeof index !== 'number') { + return null; + } if (index < 0) { return null; } - return this.parent.children.get(index + 1); + return this.parent.children?.get(index + 1); } /** * 获取上一个兄弟节点 */ - get prevSibling(): Node | null { + get prevSibling(): INode | null | undefined { if (!this.parent) { return null; } const { index } = this; + if (typeof index !== 'number') { + return null; + } if (index < 1) { return null; } - return this.parent.children.get(index - 1); + return this.parent.children?.get(index - 1); } /** * 获取符合搭建协议-节点 schema 结构 */ get schema(): Schema { - return this.export(TransformStage.Save); + return this.export(IPublicEnumTransformStage.Save); } set schema(data: Schema) { @@ -688,15 +921,15 @@ export class Node<Schema extends NodeSchema = NodeSchema> { if (this.isSlot()) { foreachReverse( this.children!, - (subNode: Node) => { + (subNode: INode) => { subNode.remove(true, true); }, - (iterable, idx) => (iterable as NodeChildren).get(idx), + (iterable, idx) => (iterable as INodeChildren).get(idx), ); } if (this.isParental()) { this.props.import(props, extras); - (this._children as NodeChildren).import(children, checkId); + this._children?.import(children, checkId); } else { this.props .get('children', true)! @@ -711,16 +944,16 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 导出 schema */ - export(stage: TransformStage = TransformStage.Save, options: any = {}): Schema { + export<T = IPublicTypeNodeSchema>(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, options: any = {}): T { stage = compatStage(stage); const baseSchema: any = { componentName: this.componentName, }; - if (stage !== TransformStage.Clone) { + if (stage !== IPublicEnumTransformStage.Clone) { baseSchema.id = this.id; } - if (stage === TransformStage.Render) { + if (stage === IPublicEnumTransformStage.Render) { baseSchema.docId = this.document.id; } @@ -753,7 +986,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { ...this.document.designer.transformProps(_extras_, this, stage), }; - if (this.isParental() && this.children.size > 0 && !options.bypassChildren) { + if (this.isParental() && this.children && this.children.size > 0 && !options.bypassChildren) { schema.children = this.children.export(stage); } @@ -763,14 +996,14 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 判断是否包含特定节点 */ - contains(node: Node): boolean { + contains(node: INode): boolean { return contains(this, node); } /** * 获取特定深度的父亲节点 */ - getZLevelTop(zLevel: number): Node | null { + getZLevelTop(zLevel: number): INode | null { return getZLevelTop(this, zLevel); } @@ -782,11 +1015,11 @@ export class Node<Schema extends NodeSchema = NodeSchema> { * 2 thisNode before or after otherNode * 0 thisNode same as otherNode */ - comparePosition(otherNode: Node): PositionNO { + comparePosition(otherNode: INode): PositionNO { return comparePosition(this, otherNode); } - unlinkSlot(slotNode: Node) { + unlinkSlot(slotNode: INode) { const i = this._slots.indexOf(slotNode); if (i < 0) { return false; @@ -797,7 +1030,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 删除一个Slot节点 */ - removeSlot(slotNode: Node, purge = false): boolean { + removeSlot(slotNode: INode): boolean { // if (purge) { // // should set parent null // slotNode?.internalSetParent(null, false); @@ -813,13 +1046,13 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return false; } - addSlot(slotNode: Node) { + addSlot(slotNode: INode) { const slotName = slotNode?.getExtraProp('name')?.getAsString(); // 一个组件下的所有 slot,相同 slotName 的 slot 应该是唯一的 if (includeSlot(this, slotName)) { removeSlot(this, slotName); } - slotNode.internalSetParent(this as ParentalNode, true); + slotNode.internalSetParent(this as INode, true); this._slots.push(slotNode); } @@ -838,19 +1071,10 @@ export class Node<Schema extends NodeSchema = NodeSchema> { * 删除一个节点 * @param node */ - removeChild(node: Node) { + removeChild(node: INode) { this.children?.delete(node); } - private purged = false; - - /** - * 是否已销毁 - */ - get isPurged() { - return this.purged; - } - /** * 销毁 */ @@ -865,30 +1089,22 @@ export class Node<Schema extends NodeSchema = NodeSchema> { // this.document.destroyNode(this); } - private purging: boolean = false; internalPurgeStart() { this.purging = true; } /** - * 是否正在销毁 - */ - get isPurging() { - return this.purging; - } - - /** - * 是否可执行某action + * 是否可执行某 action */ canPerformAction(actionName: string): boolean { const availableActions = - this.componentMeta?.availableActions?.filter((action: ComponentAction) => { + this.componentMeta?.availableActions?.filter((action: IPublicTypeComponentAction) => { const { condition } = action; return typeof condition === 'function' ? condition(this) !== false : condition !== false; }) - .map((action: ComponentAction) => action.name) || []; + .map((action: IPublicTypeComponentAction) => action.name) || []; return availableActions.indexOf(actionName) >= 0; } @@ -906,18 +1122,18 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return this.componentName; } - insert(node: Node, ref?: Node, useMutator = true) { + insert(node: INode, ref?: INode, useMutator = true) { this.insertAfter(node, ref, useMutator); } - insertBefore(node: Node, ref?: Node, useMutator = true) { + insertBefore(node: INode, ref?: INode, useMutator = true) { const nodeInstance = ensureNode(node, this.document); - this.children?.insert(nodeInstance, ref ? ref.index : null, useMutator); + this.children?.internalInsert(nodeInstance, ref ? ref.index : null, useMutator); } - insertAfter(node: any, ref?: Node, useMutator = true) { + insertAfter(node: any, ref?: INode, useMutator = true) { const nodeInstance = ensureNode(node, this.document); - this.children?.insert(nodeInstance, ref ? ref.index + 1 : null, useMutator); + this.children?.internalInsert(nodeInstance, ref ? (ref.index || 0) + 1 : null, useMutator); } getParent() { @@ -944,25 +1160,19 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return this.props; } - onChildrenChange(fn: (param?: { type: string; node: Node }) => void): (() => void) | undefined { + onChildrenChange(fn: (param?: { type: string; node: INode }) => void): IPublicTypeDisposable | undefined { const wrappedFunc = wrapWithEventSwitch(fn); return this.children?.onChange(wrappedFunc); } mergeChildren( - remover: () => any, - adder: (children: Node[]) => NodeData[] | null, - sorter: () => any, + remover: (node: INode, idx: number) => any, + adder: (children: INode[]) => IPublicTypeNodeData[] | null, + sorter: (firstNode: INode, secondNode: INode) => any, ) { this.children?.mergeChildren(remover, adder, sorter); } - @obx.shallow status: NodeStatus = { - inPlaceEditing: false, - locking: false, - pseudo: false, - }; - /** * @deprecated */ @@ -1009,27 +1219,34 @@ export class Node<Schema extends NodeSchema = NodeSchema> { /** * 获取磁贴相关信息 */ - getRGL() { + getRGL(): { + isContainerNode: boolean; + isEmptyNode: boolean; + isRGLContainerNode: boolean; + isRGLNode: boolean; + isRGL: boolean; + rglNode: Node | null; + } { const isContainerNode = this.isContainer(); const isEmptyNode = this.isEmpty(); const isRGLContainerNode = this.isRGLContainer; - const isRGLNode = this.getParent()?.isRGLContainer; + const isRGLNode = (this.getParent()?.isRGLContainer) as boolean; const isRGL = isRGLContainerNode || (isRGLNode && (!isContainerNode || !isEmptyNode)); - let rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : {}; + let rglNode = isRGLContainerNode ? this : isRGL ? this?.getParent() : null; return { isContainerNode, isEmptyNode, isRGLContainerNode, isRGLNode, isRGL, rglNode }; } /** - * @deprecated + * @deprecated no one is using this, will be removed in a future release */ - getSuitablePlace(node: Node, ref: any): any { + getSuitablePlace(node: INode, ref: any): any { const focusNode = this.document?.focusNode; // 如果节点是模态框,插入到根节点下 if (node?.componentMeta?.isModal) { return { container: focusNode, ref }; } - if (!ref && this.contains(focusNode)) { + if (!ref && focusNode && this.contains(focusNode)) { const rootCanDropIn = focusNode.componentMeta?.prototype?.options?.canDropIn; if ( rootCanDropIn === undefined || @@ -1044,7 +1261,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { if (this.isRoot() && this.children) { const dropElement = this.children.filter((c) => { - if (!c.isContainer()) { + if (!c.isContainerNode) { return false; } const canDropIn = c.componentMeta?.prototype?.options?.canDropIn; @@ -1139,11 +1356,11 @@ export class Node<Schema extends NodeSchema = NodeSchema> { return this.id; } - emitPropChange(val: PropChangeOptions) { + emitPropChange(val: IPublicTypePropChangeOptions) { this.emitter?.emit('propChange', val); } - onPropChange(func: (info: PropChangeOptions) => void): Function { + onPropChange(func: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable { const wrappedFunc = wrapWithEventSwitch(func); this.emitter.on('propChange', wrappedFunc); return () => { @@ -1152,7 +1369,7 @@ export class Node<Schema extends NodeSchema = NodeSchema> { } } -function ensureNode(node: any, document: DocumentModel): Node { +function ensureNode(node: any, document: IDocumentModel): INode { let nodeInstance = node; if (!isNode(node)) { if (node.getComponentName) { @@ -1166,33 +1383,27 @@ function ensureNode(node: any, document: DocumentModel): Node { return nodeInstance; } -export interface ParentalNode<T extends NodeSchema = NodeSchema> extends Node<T> { - readonly children: NodeChildren; -} export interface LeafNode extends Node { readonly children: null; } -export type PropChangeOptions = Omit<GlobalEvent.Node.Prop.ChangeOptions, 'node'>; - -export type SlotNode = ParentalNode<SlotSchema>; -export type PageNode = ParentalNode<PageSchema>; -export type ComponentNode = ParentalNode<ComponentSchema>; -export type RootNode = PageNode | ComponentNode; +export type IPublicTypePropChangeOptions = Omit<GlobalEvent.Node.Prop.ChangeOptions, 'node'>; -export function isNode(node: any): node is Node { - return node && node.isNode; -} +export type ISlotNode = IBaseNode<IPublicTypeSlotSchema>; +export type IPageNode = IBaseNode<IPublicTypePageSchema>; +export type IComponentNode = IBaseNode<IPublicTypeComponentSchema>; +export type IRootNode = IPageNode | IComponentNode; +export type INode = IPageNode | ISlotNode | IComponentNode | IRootNode; -export function isRootNode(node: Node): node is RootNode { - return node && node.isRoot(); +export function isRootNode(node: INode): node is IRootNode { + return node && node.isRootNode; } -export function isLowCodeComponent(node: Node): boolean { +export function isLowCodeComponent(node: INode): node is IComponentNode { return node.componentMeta?.getMetadata().devMode === 'lowCode'; } -export function getZLevelTop(child: Node, zLevel: number): Node | null { +export function getZLevelTop(child: INode, zLevel: number): INode | null { let l = child.zLevel; if (l < zLevel || zLevel < 0) { return null; @@ -1213,12 +1424,12 @@ export function getZLevelTop(child: Node, zLevel: number): Node | null { * @param node2 测试的被包含节点 * @returns 是否包含 */ -export function contains(node1: Node, node2: Node): boolean { +export function contains(node1: INode, node2: INode): boolean { if (node1 === node2) { return true; } - if (!node1.isParental() || !node2.parent) { + if (!node1.isParentalNode || !node2.parent) { return false; } @@ -1240,7 +1451,7 @@ export enum PositionNO { BeforeOrAfter = 2, TheSame = 0, } -export function comparePosition(node1: Node, node2: Node): PositionNO { +export function comparePosition(node1: INode, node2: INode): PositionNO { if (node1 === node2) { return PositionNO.TheSame; } @@ -1268,35 +1479,39 @@ export function comparePosition(node1: Node, node2: Node): PositionNO { } export function insertChild( - container: ParentalNode, - thing: Node | NodeData, + container: INode, + thing: INode | IPublicTypeNodeData, at?: number | null, copy?: boolean, -): Node { - let node: Node; - if (isNode(thing) && (copy || thing.isSlot())) { - thing = thing.export(TransformStage.Clone); - } - if (isNode(thing)) { +): INode | null { + let node: INode | null | IRootNode | undefined; + let nodeSchema: IPublicTypeNodeSchema; + if (isNode<INode>(thing) && (copy || thing.isSlot())) { + nodeSchema = thing.export(IPublicEnumTransformStage.Clone); + node = container.document?.createNode(nodeSchema); + } else if (isNode<INode>(thing)) { node = thing; - } else { - node = container.document.createNode(thing); + } else if (isNodeSchema(thing)) { + node = container.document?.createNode(thing); } - container.children.insert(node, at); + if (isNode<INode>(node)) { + container.children?.insert(node, at); + return node; + } - return node; + return null; } export function insertChildren( - container: ParentalNode, - nodes: Node[] | NodeData[], + container: INode, + nodes: INode[] | IPublicTypeNodeData[], at?: number | null, copy?: boolean, -): Node[] { +): INode[] { let index = at; let node: any; - const results: Node[] = []; + const results: INode[] = []; // eslint-disable-next-line no-cond-assign while ((node = nodes.pop())) { node = insertChild(container, node, index, copy); diff --git a/packages/designer/src/document/node/props/prop.ts b/packages/designer/src/document/node/props/prop.ts index 87fcc53103..d70f0f56ec 100644 --- a/packages/designer/src/document/node/props/prop.ts +++ b/packages/designer/src/document/node/props/prop.ts @@ -1,29 +1,64 @@ import { untracked, computed, obx, engineConfig, action, makeObservable, mobx, runInAction } from '@alilc/lowcode-editor-core'; -import { CompositeValue, GlobalEvent, isJSExpression, isJSSlot, JSSlot, SlotSchema } from '@alilc/lowcode-types'; -import { uniqueId, isPlainObject, hasOwnProperty, compatStage } from '@alilc/lowcode-utils'; +import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import type { IPublicTypeCompositeValue, IPublicTypeJSSlot, IPublicTypeSlotSchema, IPublicModelProp } from '@alilc/lowcode-types'; +import { uniqueId, isPlainObject, hasOwnProperty, compatStage, isJSExpression, isJSSlot, isNodeSchema } from '@alilc/lowcode-utils'; import { valueToSource } from './value-to-source'; -import { Props } from './props'; -import { SlotNode, Node } from '../node'; -import { TransformStage } from '../transform-stage'; +import { IPropParent } from './props'; +import type { IProps } from './props'; +import { ISlotNode, INode } from '../node'; +// import { TransformStage } from '../transform-stage'; const { set: mobxSet, isObservableArray } = mobx; export const UNSET = Symbol.for('unset'); // eslint-disable-next-line no-redeclare export type UNSET = typeof UNSET; -export interface IPropParent { - delete(prop: Prop): void; - readonly props: Props; - readonly owner: Node; - readonly path: string[]; +export interface IProp extends Omit<IPublicModelProp< + INode +>, 'exportSchema' | 'node'>, IPropParent { + spread: boolean; + + key: string | number | undefined; + + readonly props: IProps; + + readonly owner: INode; + + delete(prop: IProp): void; + + export(stage: IPublicEnumTransformStage): IPublicTypeCompositeValue; + + getNode(): INode; + + getAsString(): string; + + unset(): void; + + get value(): IPublicTypeCompositeValue | UNSET; + + compare(other: IProp | null): number; + + isUnset(): boolean; + + purge(): void; + + setupItems(): IProp[] | null; + + isVirtual(): boolean; + + get type(): ValueTypes; + + get size(): number; + + get code(): string; } export type ValueTypes = 'unset' | 'literal' | 'map' | 'list' | 'expression' | 'slot'; -export class Prop implements IPropParent { +export class Prop implements IProp, IPropParent { readonly isProp = true; - readonly owner: Node; + readonly owner: INode; /** * 键值 @@ -35,13 +70,164 @@ export class Prop implements IPropParent { */ @obx spread: boolean; - readonly props: Props; + readonly props: IProps; readonly options: any; + readonly id = uniqueId('prop$'); + + @obx.ref private _type: ValueTypes = 'unset'; + + /** + * 属性类型 + */ + get type(): ValueTypes { + return this._type; + } + + @obx private _value: any = UNSET; + + /** + * 属性值 + */ + @computed get value(): IPublicTypeCompositeValue | UNSET { + return this.export(IPublicEnumTransformStage.Serilize); + } + + private _code: string | null = null; + + /** + * 获得表达式值 + */ + @computed get code() { + if (isJSExpression(this.value)) { + return this.value.value; + } + // todo: JSFunction ... + if (this.type === 'slot') { + return JSON.stringify(this._slotNode!.export(IPublicEnumTransformStage.Save)); + } + return this._code != null ? this._code : JSON.stringify(this.value); + } + + /** + * 设置表达式值 + */ + set code(code: string) { + if (isJSExpression(this._value)) { + this.setValue({ + ...this._value, + value: code, + }); + this._code = code; + return; + } + + try { + const v = JSON.parse(code); + this.setValue(v); + this._code = code; + return; + } catch (e) { + // ignore + } + + this.setValue({ + type: 'JSExpression', + value: code, + mock: this._value, + }); + this._code = code; + } + + private _slotNode?: INode | null; + + get slotNode(): INode | null { + return this._slotNode || null; + } + + @obx.shallow private _items: IProp[] | null = null; + + /** + * 作为一层缓存机制,主要是复用部分已存在的 Prop,保持响应式关系,比如: + * 当前 Prop#_value 值为 { a: 1 },当调用 setValue({ a: 2 }) 时,所有原来的子 Prop 均被销毁, + * 导致假如外部有 mobx reaction(常见于 observer),此时响应式链路会被打断, + * 因为 reaction 监听的是原 Prop(a) 的 _value,而不是新 Prop(a) 的 _value。 + */ + @obx.shallow private _maps: Map<string | number, IProp> | null = null; + + /** + * 构造 items 属性,同时构造 maps 属性 + */ + private get items(): IProp[] | null { + if (this._items) return this._items; + return runInAction(() => { + let items: IProp[] | null = null; + if (this._type === 'list') { + const maps = new Map<string, IProp>(); + const data = this._value; + data.forEach((item: any, idx: number) => { + items = items || []; + let prop; + if (this._maps?.has(idx.toString())) { + prop = this._maps.get(idx.toString())!; + prop.setValue(item); + } else { + prop = new Prop(this, item, idx); + } + maps.set(idx.toString(), prop); + items.push(prop); + }); + this._maps = maps; + } else if (this._type === 'map') { + const data = this._value; + const maps = new Map<string, IProp>(); + const keys = Object.keys(data); + for (const key of keys) { + let prop: IProp; + if (this._maps?.has(key)) { + prop = this._maps.get(key)!; + prop.setValue(data[key]); + } else { + prop = new Prop(this, data[key], key); + } + items = items || []; + items.push(prop); + maps.set(key, prop); + } + this._maps = maps; + } else { + items = null; + this._maps = null; + } + this._items = items; + return this._items; + }); + } + + @computed private get maps(): Map<string | number, IProp> | null { + if (!this.items) { + return null; + } + return this._maps; + } + + get path(): string[] { + return (this.parent.path || []).concat(this.key as string); + } + + /** + * 元素个数 + */ + get size(): number { + return this.items?.length || 0; + } + + private purged = false; + constructor( public parent: IPropParent, - value: CompositeValue | UNSET = UNSET, + value: IPublicTypeCompositeValue | UNSET = UNSET, key?: string | number, spread = false, options = {}, @@ -88,30 +274,10 @@ export class Prop implements IPropParent { this.get(propName, false)?.unset(); } - readonly id = uniqueId('prop$'); - - @obx.ref private _type: ValueTypes = 'unset'; - - /** - * 属性类型 - */ - get type(): ValueTypes { - return this._type; - } - - @obx private _value: any = UNSET; - - /** - * 属性值 - */ - @computed get value(): CompositeValue | UNSET { - return this.export(TransformStage.Serilize); - } - - export(stage: TransformStage = TransformStage.Save): CompositeValue { + export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): IPublicTypeCompositeValue { stage = compatStage(stage); const type = this._type; - if (stage === TransformStage.Render && this.key === '___condition___') { + if (stage === IPublicEnumTransformStage.Render && this.key === '___condition___') { // 在设计器里,所有组件默认需要展示,除非开启了 enableCondition 配置 if (engineConfig?.get('enableCondition') !== true) { return true; @@ -124,20 +290,17 @@ export class Prop implements IPropParent { } if (type === 'literal' || type === 'expression') { - // TODO 后端改造之后删除此逻辑 - if (this._value === null && stage === TransformStage.Save) { - return ''; - } return this._value; } if (type === 'slot') { const schema = this._slotNode?.export(stage) || {} as any; - if (stage === TransformStage.Render) { + if (stage === IPublicEnumTransformStage.Render) { return { type: 'JSSlot', params: schema.params, value: schema, + id: schema.id, }; } return { @@ -146,6 +309,7 @@ export class Prop implements IPropParent { value: schema.children, title: schema.title, name: schema.name, + id: schema.id, }; } @@ -170,60 +334,10 @@ export class Prop implements IPropParent { if (!this._items) { return this._value; } - const values = this.items!.map((prop) => { - return prop.export(stage); - }); - if (values.every(val => val === undefined)) { - return undefined; - } - return values; - } - } - - private _code: string | null = null; - - /** - * 获得表达式值 - */ - @computed get code() { - if (isJSExpression(this.value)) { - return this.value.value; - } - // todo: JSFunction ... - if (this.type === 'slot') { - return JSON.stringify(this._slotNode!.export(TransformStage.Save)); - } - return this._code != null ? this._code : JSON.stringify(this.value); - } - - /** - * 设置表达式值 - */ - set code(code: string) { - if (isJSExpression(this._value)) { - this.setValue({ - ...this._value, - value: code, + return this.items!.map((prop) => { + return prop?.export(stage); }); - this._code = code; - return; - } - - try { - const v = JSON.parse(code); - this.setValue(v); - this._code = code; - return; - } catch (e) { - // ignore } - - this.setValue({ - type: 'JSExpression', - value: code, - mock: this._value, - }); - this._code = code; } getAsString(): string { @@ -237,9 +351,8 @@ export class Prop implements IPropParent { * set value, val should be JSON Object */ @action - setValue(val: CompositeValue) { + 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; @@ -268,26 +381,37 @@ export class Prop implements IPropParent { } this.dispose(); + // setValue 的时候,如果不重新建立 items,items 的 setValue 没有触发,会导致子项的响应式逻辑不能被触发 + this.setupItems(); if (oldValue !== this._value) { - const propsInfo = { - key: this.key, - prop: this, - oldValue, - newValue: this._value, - }; - - editor?.emit(GlobalEvent.Node.Prop.InnerChange, { - node: this.owner as any, - ...propsInfo, - }); - - this.owner?.emitPropChange?.(propsInfo); + this.emitChange({ oldValue }); } } - getValue(): CompositeValue { - return this.export(TransformStage.Serilize); + 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); } @action @@ -297,38 +421,47 @@ export class Prop implements IPropParent { items.forEach((prop) => prop.purge()); } this._items = null; - this._prevMaps = this._maps; - this._maps = null; if (this._type !== 'slot' && this._slotNode) { this._slotNode.remove(); this._slotNode = undefined; } } - private _slotNode?: SlotNode; - - get slotNode() { - return this._slotNode; - } - @action - setAsSlot(data: JSSlot) { + setAsSlot(data: IPublicTypeJSSlot) { this._type = 'slot'; - const slotSchema: SlotSchema = { - componentName: 'Slot', - title: data.title, - id: data.id, - name: data.name, - params: data.params, - children: data.value, - }; + let slotSchema: IPublicTypeSlotSchema; + // 当 data.value 的结构为 { componentName: 'Slot' } 时,复用部分 slotSchema 数据 + if ((isPlainObject(data.value) && isNodeSchema(data.value) && data.value?.componentName === 'Slot')) { + const value = data.value as IPublicTypeSlotSchema; + slotSchema = { + componentName: 'Slot', + title: value.title || value.props?.slotTitle, + id: value.id, + name: value.name || value.props?.slotName, + params: value.params || value.props?.slotParams, + children: value.children, + } as IPublicTypeSlotSchema; + } else { + slotSchema = { + componentName: 'Slot', + title: data.title, + id: data.id, + name: data.name, + params: data.params, + children: data.value, + }; + } + if (this._slotNode) { this._slotNode.import(slotSchema); } else { const { owner } = this.props; - this._slotNode = owner.document.createNode<SlotNode>(slotSchema); - owner.addSlot(this._slotNode); - this._slotNode.internalSetSlotFor(this); + this._slotNode = owner.document?.createNode<ISlotNode>(slotSchema); + if (this._slotNode) { + owner.addSlot(this._slotNode); + this._slotNode.internalSetSlotFor(this); + } } } @@ -337,7 +470,12 @@ export class Prop implements IPropParent { */ @action unset() { - this._type = 'unset'; + if (this._type !== 'unset') { + this._type = 'unset'; + this.emitChange({ + oldValue: this._value, + }); + } } /** @@ -355,7 +493,7 @@ export class Prop implements IPropParent { /** * @returns 0: the same 1: maybe & like 2: not the same */ - compare(other: Prop | null): number { + compare(other: IProp | null): number { if (!other || other.isUnset()) { return this.isUnset() ? 0 : 2; } @@ -374,75 +512,12 @@ export class Prop implements IPropParent { return this.code === other.code ? 0 : 2; } - @obx.shallow private _items: Prop[] | null = null; - - @obx.shallow private _maps: Map<string | number, Prop> | null = null; - - /** - * 作为 _maps 的一层缓存机制,主要是复用部分已存在的 Prop,保持响应式关系,比如: - * 当前 Prop#_value 值为 { a: 1 },当调用 setValue({ a: 2 }) 时,所有原来的子 Prop 均被销毁, - * 导致假如外部有 mobx reaction(常见于 observer),此时响应式链路会被打断, - * 因为 reaction 监听的是原 Prop(a) 的 _value,而不是新 Prop(a) 的 _value。 - */ - private _prevMaps: Map<string | number, Prop> | null = null; - - get path(): string[] { - return (this.parent.path || []).concat(this.key as string); - } - - /** - * 构造 items 属性,同时构造 maps 属性 - */ - private get items(): Prop[] | null { - if (this._items) return this._items; - return runInAction(() => { - let items: Prop[] | null = null; - if (this._type === 'list') { - const data = this._value; - data.forEach((item: any, idx: number) => { - items = items || []; - items.push(new Prop(this, item, idx)); - }); - this._maps = null; - } else if (this._type === 'map') { - const data = this._value; - const maps = new Map<string, Prop>(); - const keys = Object.keys(data); - for (const key of keys) { - let prop: Prop; - if (this._prevMaps?.has(key)) { - prop = this._prevMaps.get(key)!; - prop.setValue(data[key]); - } else { - prop = new Prop(this, data[key], key); - } - items = items || []; - items.push(prop); - maps.set(key, prop); - } - this._maps = maps; - } else { - items = null; - this._maps = null; - } - this._items = items; - return this._items; - }); - } - - @computed private get maps(): Map<string | number, Prop> | null { - if (!this.items) { - return null; - } - return this._maps; - } - /** * 获取某个属性 * @param createIfNone 当没有的时候,是否创建一个 */ @action - get(path: string | number, createIfNone = true): Prop | null { + get(path: string | number, createIfNone = true): IProp | null { const type = this._type; if (type !== 'map' && type !== 'list' && type !== 'unset' && !createIfNone) { return null; @@ -495,13 +570,14 @@ export class Prop implements IPropParent { @action remove() { this.parent.delete(this); + this.unset(); } /** * 删除项 */ @action - delete(prop: Prop): void { + delete(prop: IProp): void { /* istanbul ignore else */ if (this._items) { const i = this._items.indexOf(prop); @@ -529,20 +605,13 @@ export class Prop implements IPropParent { } } - /** - * 元素个数 - */ - get size(): number { - return this.items?.length || 0; - } - /** * 添加值到列表 * * @param force 强制 */ @action - add(value: CompositeValue, force = false): Prop | null { + add(value: IPublicTypeCompositeValue, force = false): IProp | null { const type = this._type; if (type !== 'list' && type !== 'unset' && !force) { return null; @@ -562,7 +631,7 @@ export class Prop implements IPropParent { * @param force 强制 */ @action - set(key: string | number, value: CompositeValue | Prop, force = false) { + set(key: string | number, value: IPublicTypeCompositeValue | Prop, force = false) { const type = this._type; if (type !== 'map' && type !== 'list' && type !== 'unset' && !force) { return null; @@ -625,8 +694,6 @@ export class Prop implements IPropParent { return hasOwnProperty(this._value, key); } - private purged = false; - /** * 回收销毁 */ @@ -650,7 +717,7 @@ export class Prop implements IPropParent { /** * 迭代器 */ - [Symbol.iterator](): { next(): { value: Prop } } { + [Symbol.iterator](): { next(): { value: IProp } } { let index = 0; const { items } = this; const length = items?.length || 0; @@ -674,7 +741,7 @@ export class Prop implements IPropParent { * 遍历 */ @action - forEach(fn: (item: Prop, key: number | string | undefined) => void): void { + forEach(fn: (item: IProp, key: number | string | undefined) => void): void { const { items } = this; if (!items) { return; @@ -689,7 +756,7 @@ export class Prop implements IPropParent { * 遍历 */ @action - map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null { + map<T>(fn: (item: IProp, key: number | string | undefined) => T): T[] | null { const { items } = this; if (!items) { return null; diff --git a/packages/designer/src/document/node/props/props.ts b/packages/designer/src/document/node/props/props.ts index 1ec6df9703..213592a5de 100644 --- a/packages/designer/src/document/node/props/props.ts +++ b/packages/designer/src/document/node/props/props.ts @@ -1,9 +1,11 @@ import { computed, makeObservable, obx, action } from '@alilc/lowcode-editor-core'; -import { PropsMap, PropsList, CompositeValue } from '@alilc/lowcode-types'; +import { IPublicTypePropsList, IPublicTypeCompositeValue, IPublicEnumTransformStage, IBaseModelProps } from '@alilc/lowcode-types'; +import type { IPublicTypePropsMap } from '@alilc/lowcode-types'; import { uniqueId, compatStage } from '@alilc/lowcode-utils'; -import { Prop, IPropParent, UNSET } from './prop'; -import { Node } from '../node'; -import { TransformStage } from '../transform-stage'; +import { Prop, UNSET } from './prop'; +import type { IProp } from './prop'; +import { INode } from '../node'; +// import { TransformStage } from '../transform-stage'; interface ExtrasObject { [key: string]: any; @@ -23,10 +25,45 @@ export function getConvertedExtraKey(key: string): string { export function getOriginalExtraKey(key: string): string { return key.replace(new RegExp(`${EXTRA_KEY_PREFIX}`, 'g'), ''); } -export class Props implements IPropParent { + +export interface IPropParent { + + readonly props: IProps; + + readonly owner: INode; + + get path(): string[]; + + delete(prop: IProp): void; +} + +export interface IProps extends Omit<IBaseModelProps<IProp>, | 'getExtraProp' | 'getExtraPropValue' | 'setExtraPropValue' | 'node'>, IPropParent { + + /** + * 获取 props 对应的 node + */ + getNode(): INode; + + get(path: string, createIfNone?: boolean): IProp | null; + + export(stage?: IPublicEnumTransformStage): { + props?: IPublicTypePropsMap | IPublicTypePropsList; + extras?: ExtrasObject; + }; + + merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap): void; + + purge(): void; + + query(path: string, createIfNone: boolean): IProp | null; + + import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject): void; +} + +export class Props implements IProps, IPropParent { readonly id = uniqueId('props'); - @obx.shallow private items: Prop[] = []; + @obx.shallow private items: IProp[] = []; @computed private get maps(): Map<string, Prop> { const maps = new Map(); @@ -42,11 +79,11 @@ export class Props implements IPropParent { readonly path = []; - get props(): Props { + get props(): IProps { return this; } - readonly owner: Node; + readonly owner: INode; /** * 元素个数 @@ -57,7 +94,9 @@ export class Props implements IPropParent { @obx type: 'map' | 'list' = 'map'; - constructor(owner: Node, value?: PropsMap | PropsList | null, extras?: ExtrasObject) { + private purged = false; + + constructor(owner: INode, value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject) { makeObservable(this); this.owner = owner; if (Array.isArray(value)) { @@ -76,7 +115,7 @@ export class Props implements IPropParent { } @action - import(value?: PropsMap | PropsList | null, extras?: ExtrasObject) { + import(value?: IPublicTypePropsMap | IPublicTypePropsList | null, extras?: ExtrasObject) { const originItems = this.items; if (Array.isArray(value)) { this.type = 'list'; @@ -99,7 +138,7 @@ export class Props implements IPropParent { } @action - merge(value: PropsMap, extras?: PropsMap) { + merge(value: IPublicTypePropsMap, extras?: IPublicTypePropsMap) { Object.keys(value).forEach((key) => { this.query(key, true)!.setValue(value[key]); this.query(key, true)!.setupItems(); @@ -112,8 +151,8 @@ export class Props implements IPropParent { } } - export(stage: TransformStage = TransformStage.Save): { - props?: PropsMap | PropsList; + export(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save): { + props?: IPublicTypePropsMap | IPublicTypePropsList; extras?: ExtrasObject; } { stage = compatStage(stage); @@ -191,16 +230,16 @@ export class Props implements IPropParent { * @param createIfNone 当没有的时候,是否创建一个 */ @action - query(path: string, createIfNone = true): Prop | null { + query(path: string, createIfNone = true): IProp | null { return this.get(path, createIfNone); } /** - * 获取某个属性, 如果不存在,临时获取一个待写入 + * 获取某个属性,如果不存在,临时获取一个待写入 * @param createIfNone 当没有的时候,是否创建一个 */ @action - get(path: string, createIfNone = false): Prop | null { + get(path: string, createIfNone = false): IProp | null { let entry = path; let nest = ''; const i = path.indexOf('.'); @@ -228,7 +267,7 @@ export class Props implements IPropParent { * 删除项 */ @action - delete(prop: Prop): void { + delete(prop: IProp): void { const i = this.items.indexOf(prop); if (i > -1) { this.items.splice(i, 1); @@ -256,11 +295,11 @@ export class Props implements IPropParent { */ @action add( - value: CompositeValue | null, + value: IPublicTypeCompositeValue | null, key?: string | number, spread = false, options: any = {}, - ): Prop { + ): IProp { const prop = new Prop(this, value, key, spread, options); this.items.push(prop); return prop; @@ -276,7 +315,7 @@ export class Props implements IPropParent { /** * 迭代器 */ - [Symbol.iterator](): { next(): { value: Prop } } { + [Symbol.iterator](): { next(): { value: IProp } } { let index = 0; const { items } = this; const length = items.length || 0; @@ -300,7 +339,7 @@ export class Props implements IPropParent { * 遍历 */ @action - forEach(fn: (item: Prop, key: number | string | undefined) => void): void { + forEach(fn: (item: IProp, key: number | string | undefined) => void): void { this.items.forEach((item) => { return fn(item, item.key); }); @@ -310,21 +349,19 @@ export class Props implements IPropParent { * 遍历 */ @action - map<T>(fn: (item: Prop, key: number | string | undefined) => T): T[] | null { + map<T>(fn: (item: IProp, key: number | string | undefined) => T): T[] | null { return this.items.map((item) => { return fn(item, item.key); }); } @action - filter(fn: (item: Prop, key: number | string | undefined) => boolean) { + filter(fn: (item: IProp, key: number | string | undefined) => boolean) { return this.items.filter((item) => { return fn(item, item.key); }); } - private purged = false; - /** * 回收销毁 */ @@ -342,7 +379,7 @@ export class Props implements IPropParent { * @param createIfNone 当没有的时候,是否创建一个 */ @action - getProp(path: string, createIfNone = true): Prop | null { + getProp(path: string, createIfNone = true): IProp | null { return this.query(path, createIfNone) || null; } diff --git a/packages/designer/src/document/node/props/value-to-source.ts b/packages/designer/src/document/node/props/value-to-source.ts index a3ad81b751..7d7de7f76d 100644 --- a/packages/designer/src/document/node/props/value-to-source.ts +++ b/packages/designer/src/document/node/props/value-to-source.ts @@ -212,6 +212,8 @@ export function valueToSource( } case 'undefined': return `${indentString.repeat(indentLevel)}undefined`; + default: + return `${indentString.repeat(indentLevel)}undefined`; } } diff --git a/packages/designer/src/document/selection.ts b/packages/designer/src/document/selection.ts index bd30f53149..6147e188d8 100644 --- a/packages/designer/src/document/selection.ts +++ b/packages/designer/src/document/selection.ts @@ -1,10 +1,14 @@ -import { EventEmitter } from 'events'; -import { obx, makeObservable } from '@alilc/lowcode-editor-core'; -import { Node, comparePosition, PositionNO } from './node/node'; +import { obx, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { INode, comparePosition, PositionNO } from './node/node'; import { DocumentModel } from './document-model'; +import { IPublicModelSelection } from '@alilc/lowcode-types'; -export class Selection { - private emitter = new EventEmitter(); +export interface ISelection extends Omit<IPublicModelSelection<INode>, 'node'> { + containsNode(node: INode, excludeRoot: boolean): boolean; +} + +export class Selection implements ISelection { + private emitter: IEventBus = createModuleEventBus('Selection'); @obx.shallow private _selected: string[] = []; @@ -28,6 +32,12 @@ export class Selection { return; } + const node = this.doc.getNode(id); + + if (!node?.canSelect()) { + return; + } + this._selected = [id]; this.emitter.emit('selectionchange', this._selected); } @@ -36,7 +46,18 @@ export class Selection { * 批量选中 */ selectAll(ids: string[]) { - this._selected = ids; + const selectIds: string[] = []; + + ids.forEach(d => { + const node = this.doc.getNode(d); + + if (node?.canSelect()) { + selectIds.push(d); + } + }); + + this._selected = selectIds; + this.emitter.emit('selectionchange', this._selected); } @@ -101,7 +122,7 @@ export class Selection { /** * 选区是否包含节点 */ - containsNode(node: Node, excludeRoot = false) { + containsNode(node: INode, excludeRoot = false) { for (const id of this._selected) { const parent = this.doc.getNode(id); if (excludeRoot && parent?.contains(this.doc.focusNode)) { @@ -117,8 +138,8 @@ export class Selection { /** * 获取选中的节点 */ - getNodes(): Node[] { - const nodes = []; + getNodes(): INode[] { + const nodes: INode[] = []; for (const id of this._selected) { const node = this.doc.getNode(id); if (node) { @@ -129,7 +150,7 @@ export class Selection { } /** - * 获取顶层选区节点, 场景:拖拽时,建立蒙层,只蒙在最上层 + * 获取顶层选区节点,场景:拖拽时,建立蒙层,只蒙在最上层 */ getTopNodes(includeRoot = false) { const nodes = []; diff --git a/packages/designer/src/icons/index.ts b/packages/designer/src/icons/index.ts index f018646046..0a73bbc537 100644 --- a/packages/designer/src/icons/index.ts +++ b/packages/designer/src/icons/index.ts @@ -7,4 +7,3 @@ export * from './clone'; export * from './page'; export * from './container'; export * from './unlock'; - diff --git a/packages/designer/src/index.ts b/packages/designer/src/index.ts index 489d81482f..11e6453b8a 100644 --- a/packages/designer/src/index.ts +++ b/packages/designer/src/index.ts @@ -6,3 +6,4 @@ export * from './project'; export * from './builtin-simulator'; export * from './plugin'; export * from './types'; +export * from './context-menu-actions'; diff --git a/packages/designer/src/less-variables.less b/packages/designer/src/less-variables.less index c44fc196e2..017e432ce6 100644 --- a/packages/designer/src/less-variables.less +++ b/packages/designer/src/less-variables.less @@ -99,19 +99,19 @@ @brand-link-hover: #2e76a6; // F1-1-7 A10 -@brand-danger-alpha-7: rgba(240, 70, 49, 0.9); +@brand-danger-alpha-7: rgba(240, 70, 49, 0.1); // F1-1-8 A6 @brand-danger-alpha-8: rgba(240, 70, 49, 0.8); // F2-1-2 A80 @brand-warning-alpha-2: rgba(250, 189, 14, 0.8); // F2-1-7 A10 -@brand-warning-alpha-7: rgba(250, 189, 14, 0.9); +@brand-warning-alpha-7: rgba(250, 189, 14, 0.1); // F3-1-2 A80 @brand-success-alpha-2: rgba(102, 188, 92, 0.8); // F3-1-7 A10 -@brand-success-alpha-7: rgba(102, 188, 92, 0.9); +@brand-success-alpha-7: rgba(102, 188, 92, 0.1); // F4-1-7 A10 -@brand-link-alpha-7: rgba(102, 188, 92, 0.9); +@brand-link-alpha-7: rgba(102, 188, 92, 0.1); // 文本色 @text-primary-color: @dark-alpha-3; diff --git a/packages/designer/src/locale/en-US.json b/packages/designer/src/locale/en-US.json index cb96a5bc7f..28b489c8a5 100644 --- a/packages/designer/src/locale/en-US.json +++ b/packages/designer/src/locale/en-US.json @@ -5,5 +5,7 @@ "lock": "Lock", "unlock": "Unlock", "Condition Group": "Condition Group", - "No opened document": "No opened document, open some document to editing" + "No opened document": "No opened document, open some document to editing", + "locked": "locked", + "Item": "Item" } diff --git a/packages/designer/src/locale/index.ts b/packages/designer/src/locale/index.ts index a912240fa3..4cb3b53cfb 100644 --- a/packages/designer/src/locale/index.ts +++ b/packages/designer/src/locale/index.ts @@ -1,10 +1,10 @@ import { createIntl } from '@alilc/lowcode-editor-core'; -import en_US from './en-US.json'; -import zh_CN from './zh-CN.json'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; const { intl, intlNode, getLocale, setLocale } = createIntl({ - 'en-US': en_US, - 'zh-CN': zh_CN, + 'en-US': enUS, + 'zh-CN': zhCN, }); export { intl, intlNode, getLocale, setLocale }; diff --git a/packages/designer/src/locale/zh-CN.json b/packages/designer/src/locale/zh-CN.json index 0caf4fef0d..6ecf797864 100644 --- a/packages/designer/src/locale/zh-CN.json +++ b/packages/designer/src/locale/zh-CN.json @@ -5,5 +5,7 @@ "lock": "锁定", "unlock": "解锁", "Condition Group": "条件组", - "No opened document": "没有打开的页面,请选择页面打开编辑" + "No opened document": "没有打开的页面,请选择页面打开编辑", + "locked": "已锁定", + "Item": "项目" } diff --git a/packages/designer/src/plugin/plugin-context.ts b/packages/designer/src/plugin/plugin-context.ts index becae741fe..7f26a2b4f3 100644 --- a/packages/designer/src/plugin/plugin-context.ts +++ b/packages/designer/src/plugin/plugin-context.ts @@ -1,60 +1,61 @@ /* eslint-disable no-multi-assign */ -import { Editor, EngineConfig, engineConfig } from '@alilc/lowcode-editor-core'; -import { Designer, ILowCodePluginManager } from '@alilc/lowcode-designer'; -import { Skeleton as InnerSkeleton } from '@alilc/lowcode-editor-skeleton'; +import { engineConfig, createModuleEventBus } from '@alilc/lowcode-editor-core'; import { - Hotkey, - Project, - Skeleton, - Setters, - Material, - Event, - editorSymbol, - designerSymbol, - skeletonSymbol, -} from '@alilc/lowcode-shell'; -import { getLogger, Logger } from '@alilc/lowcode-utils'; + IPublicApiHotkey, + IPublicApiProject, + IPublicApiSkeleton, + IPublicApiSetters, + IPublicApiMaterial, + IPublicApiEvent, + IPublicApiCommon, + IPublicModelPluginContext, + IPluginPreferenceMananger, + IPublicTypePreferenceValueType, + IPublicModelEngineConfig, + IPublicApiLogger, + IPublicApiPlugins, + IPublicTypePluginDeclaration, + IPublicApiCanvas, + IPublicApiWorkspace, + IPublicEnumPluginRegisterLevel, + IPublicModelWindow, + IPublicApiCommonUI, +} from '@alilc/lowcode-types'; import { - ILowCodePluginContext, IPluginContextOptions, - ILowCodePluginPreferenceDeclaration, - PreferenceValueType, - IPluginPreferenceMananger, + ILowCodePluginContextApiAssembler, + ILowCodePluginContextPrivate, } from './plugin-types'; import { isValidPreferenceKey } from './plugin-utils'; -export default class PluginContext implements ILowCodePluginContext { - private readonly [editorSymbol]: Editor; - private readonly [designerSymbol]: Designer; - private readonly [skeletonSymbol]: InnerSkeleton; - hotkey: Hotkey; - project: Project; - skeleton: Skeleton; - logger: Logger; - setters: Setters; - material: Material; - config: EngineConfig; - event: Event; - plugins: ILowCodePluginManager; +export default class PluginContext implements + IPublicModelPluginContext, ILowCodePluginContextPrivate { + hotkey: IPublicApiHotkey; + project: IPublicApiProject; + skeleton: IPublicApiSkeleton; + setters: IPublicApiSetters; + material: IPublicApiMaterial; + event: IPublicApiEvent; + config: IPublicModelEngineConfig; + common: IPublicApiCommon; + logger: IPublicApiLogger; + plugins: IPublicApiPlugins; preference: IPluginPreferenceMananger; + pluginEvent: IPublicApiEvent; + canvas: IPublicApiCanvas; + workspace: IPublicApiWorkspace; + registerLevel: IPublicEnumPluginRegisterLevel; + editorWindow: IPublicModelWindow; + commonUI: IPublicApiCommonUI; + isPluginRegisteredInWorkspace: false; - constructor(plugins: ILowCodePluginManager, options: IPluginContextOptions) { - const editor = this[editorSymbol] = plugins.editor; - const designer = this[designerSymbol] = editor.get('designer')!; - const skeleton = this[skeletonSymbol] = editor.get('skeleton')!; - - const { pluginName = 'anonymous' } = options; - const project = designer?.project; - this.hotkey = new Hotkey(); - this.project = new Project(project); - this.skeleton = new Skeleton(skeleton); - this.setters = new Setters(); - this.material = new Material(editor); - this.config = engineConfig; - this.plugins = plugins; - this.event = new Event(editor, { prefix: 'common' }); - this.logger = getLogger({ level: 'warn', bizName: `designer:plugin:${pluginName}` }); - + constructor( + options: IPluginContextOptions, + contextApiAssembler: ILowCodePluginContextApiAssembler, + ) { + const { pluginName = 'anonymous', meta = {} } = options; + contextApiAssembler.assembleApis(this, pluginName, meta); + this.pluginEvent = createModuleEventBus(pluginName, 200); const enhancePluginContextHook = engineConfig.get('enhancePluginContextHook'); if (enhancePluginContextHook) { enhancePluginContextHook(this); @@ -63,12 +64,12 @@ export default class PluginContext implements ILowCodePluginContext { setPreference( pluginName: string, - preferenceDeclaration: ILowCodePluginPreferenceDeclaration, + preferenceDeclaration: IPublicTypePluginDeclaration, ): void { const getPreferenceValue = ( key: string, - defaultValue?: PreferenceValueType, - ): PreferenceValueType | undefined => { + defaultValue?: IPublicTypePreferenceValueType, + ): IPublicTypePreferenceValueType | undefined => { if (!isValidPreferenceKey(key, preferenceDeclaration)) { return undefined; } diff --git a/packages/designer/src/plugin/plugin-manager.ts b/packages/designer/src/plugin/plugin-manager.ts index dc803ddfec..f6b325dc0d 100644 --- a/packages/designer/src/plugin/plugin-manager.ts +++ b/packages/designer/src/plugin/plugin-manager.ts @@ -1,43 +1,54 @@ -import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; +import { engineConfig } from '@alilc/lowcode-editor-core'; import { getLogger } from '@alilc/lowcode-utils'; import { - ILowCodePlugin, - ILowCodePluginConfig, + ILowCodePluginRuntime, ILowCodePluginManager, - ILowCodePluginContext, - ILowCodeRegisterOptions, IPluginContextOptions, - PreferenceValueType, - ILowCodePluginConfigMeta, PluginPreference, - ILowCodePluginPreferenceDeclaration, - isLowCodeRegisterOptions, + ILowCodePluginContextApiAssembler, } from './plugin-types'; -import { filterValidOptions } from './plugin-utils'; -import { LowCodePlugin } from './plugin'; +import { filterValidOptions, isLowCodeRegisterOptions } from './plugin-utils'; +import { LowCodePluginRuntime } from './plugin'; +// eslint-disable-next-line import/no-named-as-default import LowCodePluginContext from './plugin-context'; import { invariant } from '../utils'; import sequencify from './sequencify'; import semverSatisfies from 'semver/functions/satisfies'; +import { + IPublicTypePluginRegisterOptions, + IPublicTypePreferenceValueType, + IPublicTypePlugin, +} from '@alilc/lowcode-types'; const logger = getLogger({ level: 'warn', bizName: 'designer:pluginManager' }); +// 保留的事件前缀 +const RESERVED_EVENT_PREFIX = ['designer', 'editor', 'skeleton', 'renderer', 'render', 'utils', 'plugin', 'engine', 'editor-core', 'engine-core', 'plugins', 'event', 'events', 'log', 'logger', 'ctx', 'context']; + export class LowCodePluginManager implements ILowCodePluginManager { - private plugins: ILowCodePlugin[] = []; + private plugins: ILowCodePluginRuntime[] = []; - private pluginsMap: Map<string, ILowCodePlugin> = new Map(); + pluginsMap: Map<string, ILowCodePluginRuntime> = new Map(); + pluginContextMap: Map<string, LowCodePluginContext> = new Map(); private pluginPreference?: PluginPreference = new Map(); - private editor: Editor; - constructor(editor: Editor) { - this.editor = editor; - } + contextApiAssembler: ILowCodePluginContextApiAssembler; - private _getLowCodePluginContext(options: IPluginContextOptions) { - return new LowCodePluginContext(this, options); + constructor(contextApiAssembler: ILowCodePluginContextApiAssembler, readonly viewName = 'global') { + this.contextApiAssembler = contextApiAssembler; } + _getLowCodePluginContext = (options: IPluginContextOptions) => { + const { pluginName } = options; + let context = this.pluginContextMap.get(pluginName); + if (!context) { + context = new LowCodePluginContext(options, this.contextApiAssembler); + this.pluginContextMap.set(pluginName, context); + } + return context; + }; + isEngineVersionMatched(versionExp: string): boolean { const engineVersion = engineConfig.get('ENGINE_VERSION'); // ref: https://github.com/npm/node-semver#functions @@ -52,20 +63,30 @@ export class LowCodePluginManager implements ILowCodePluginManager { * @param registerOptions - the plugin register options */ async register( - pluginConfigCreator: (ctx: ILowCodePluginContext, options: any) => ILowCodePluginConfig, + pluginModel: IPublicTypePlugin, options?: any, - registerOptions?: ILowCodeRegisterOptions, + registerOptions?: IPublicTypePluginRegisterOptions, ): Promise<void> { // registerOptions maybe in the second place if (isLowCodeRegisterOptions(options)) { registerOptions = options; options = {}; } - let { pluginName, meta = {} } = pluginConfigCreator as any; - const { preferenceDeclaration, engines } = meta as ILowCodePluginConfigMeta; - const ctx = this._getLowCodePluginContext({ pluginName }); + let { pluginName, meta = {} } = pluginModel; + const { preferenceDeclaration, engines } = meta; + // filter invalid eventPrefix + const { eventPrefix } = meta; + const isReservedPrefix = RESERVED_EVENT_PREFIX.find((item) => item === eventPrefix); + if (isReservedPrefix) { + meta.eventPrefix = undefined; + logger.warn(`plugin ${pluginName} is trying to use ${eventPrefix} as event prefix, which is a reserved event prefix, please use another one`); + } + const ctx = this._getLowCodePluginContext({ pluginName, meta }); const customFilterValidOptions = engineConfig.get('customPluginFilterOptions', filterValidOptions); - const config = pluginConfigCreator(ctx, customFilterValidOptions(options, preferenceDeclaration!)); + const pluginTransducer = engineConfig.get('customPluginTransducer', null); + const newPluginModel = pluginTransducer ? await pluginTransducer(pluginModel, ctx, options) : pluginModel; + const newOptions = customFilterValidOptions(options, newPluginModel.meta?.preferenceDeclaration); + const config = newPluginModel(ctx, newOptions); // compat the legacy way to declare pluginName // @ts-ignore pluginName = pluginName || config.name; @@ -75,7 +96,7 @@ export class LowCodePluginManager implements ILowCodePluginManager { config, ); - ctx.setPreference(pluginName, (preferenceDeclaration as ILowCodePluginPreferenceDeclaration)); + ctx.setPreference(pluginName, preferenceDeclaration); const allowOverride = registerOptions?.override === true; @@ -101,21 +122,22 @@ export class LowCodePluginManager implements ILowCodePluginManager { throw new Error(`plugin ${pluginName} skipped, engine check failed, current engine version is ${engineConfig.get('ENGINE_VERSION')}, meta.engines.lowcodeEngine is ${engineVersionExp}`); } - const plugin = new LowCodePlugin(pluginName, this, config, meta); - // support initialization of those plugins which registered after normal initialization by plugin-manager + const plugin = new LowCodePluginRuntime(pluginName, this, config, meta); + // support initialization of those plugins which registered + // after normal initialization by plugin-manager if (registerOptions?.autoInit) { await plugin.init(); } this.plugins.push(plugin); this.pluginsMap.set(pluginName, plugin); - logger.log(`plugin registered with pluginName: ${pluginName}, config: ${config}, meta: ${meta}`); + logger.log(`plugin registered with pluginName: ${pluginName}, config: `, config, 'meta:', meta); } - get(pluginName: string): ILowCodePlugin | undefined { + get(pluginName: string): ILowCodePluginRuntime | undefined { return this.pluginsMap.get(pluginName); } - getAll(): ILowCodePlugin[] { + getAll(): ILowCodePluginRuntime[] { return this.plugins; } @@ -124,18 +146,17 @@ export class LowCodePluginManager implements ILowCodePluginManager { } async delete(pluginName: string): Promise<boolean> { - const idx = this.plugins.findIndex((plugin) => plugin.name === pluginName); - if (idx === -1) return false; - const plugin = this.plugins[idx]; + const plugin = this.plugins.find(({ name }) => name === pluginName); + if (!plugin) return false; await plugin.destroy(); - + const idx = this.plugins.indexOf(plugin); this.plugins.splice(idx, 1); return this.pluginsMap.delete(pluginName); } async init(pluginPreference?: PluginPreference) { const pluginNames: string[] = []; - const pluginObj: { [name: string]: ILowCodePlugin } = {}; + const pluginObj: { [name: string]: ILowCodePluginRuntime } = {}; this.pluginPreference = pluginPreference; this.plugins.forEach((plugin) => { pluginNames.push(plugin.name); @@ -167,7 +188,7 @@ export class LowCodePluginManager implements ILowCodePluginManager { return this.pluginsMap.size; } - getPluginPreference(pluginName: string): Record<string, PreferenceValueType> | null | undefined { + getPluginPreference(pluginName: string): Record<string, IPublicTypePreferenceValueType> | null | undefined { if (!this.pluginPreference) { return null; } diff --git a/packages/designer/src/plugin/plugin-types.ts b/packages/designer/src/plugin/plugin-types.ts index 1bfb5aa316..cfc38866f5 100644 --- a/packages/designer/src/plugin/plugin-types.ts +++ b/packages/designer/src/plugin/plugin-types.ts @@ -1,78 +1,37 @@ -import { CompositeObject, ComponentAction } from '@alilc/lowcode-types'; -import Logger from 'zen-logger'; import { - Hotkey, - Skeleton, - Project, - Event, Material, -} from '@alilc/lowcode-shell'; -import { EngineConfig } from '@alilc/lowcode-editor-core'; -import { MetadataTransducer } from '@alilc/lowcode-designer'; -import { Setters } from '../types'; - -export type PreferenceValueType = string | number | boolean; - -export interface ILowCodePluginPreferenceDeclarationProperty { - // shape like 'name' or 'group.name' or 'group.subGroup.name' - key: string; - // must have either one of description & markdownDescription - description: string; - // value in 'number', 'string', 'boolean' - type: string; - // default value - // NOTE! this is only used in configuration UI, won`t affect runtime - default?: PreferenceValueType; - // only works when type === 'string', default value false - useMultipleLineTextInput?: boolean; - // enum values, only works when type === 'string' - enum?: any[]; - // descriptions for enum values - enumDescriptions?: string[]; - // message that describing deprecation of this property - deprecationMessage?: string; -} - -/** - * declaration of plugin`s preference - * when strictPluginMode === true, only declared preference can be obtained from inside plugin. - * - * @export - * @interface ILowCodePluginPreferenceDeclaration - */ -export interface ILowCodePluginPreferenceDeclaration { - // this will be displayed on configuration UI, can be plugin name - title: string; - properties: ILowCodePluginPreferenceDeclarationProperty[]; -} - -export type PluginPreference = Map<string, Record<string, PreferenceValueType>>; - -export interface ILowCodePluginConfig { - dep?: string | string[]; - init?(): void; - destroy?(): void; - exports?(): any; -} - -export interface ILowCodePluginConfigMetaEngineConfig { - lowcodeEngine?: string; -} -export interface ILowCodePluginConfigMeta { - preferenceDeclaration?: ILowCodePluginPreferenceDeclaration; - // 依赖插件名 - dependencies?: string[]; - engines?: ILowCodePluginConfigMetaEngineConfig; -} - -export interface ILowCodePluginCore { + IPublicApiHotkey, + IPublicApiProject, + IPublicApiSkeleton, + IPublicApiSetters, + IPublicApiMaterial, + IPublicApiEvent, + IPublicApiCommon, + IPublicApiPlugins, + IPublicTypePluginConfig, + IPublicApiLogger, + IPublicTypePreferenceValueType, + IPublicModelEngineConfig, + IPublicTypePlugin, + IPublicApiCanvas, + IPublicApiWorkspace, + IPublicTypePluginMeta, + IPublicTypePluginRegisterOptions, + IPublicModelWindow, + IPublicEnumPluginRegisterLevel, + IPublicApiCommonUI, + IPublicApiCommand, +} from '@alilc/lowcode-types'; +import PluginContext from './plugin-context'; + +export type PluginPreference = Map<string, Record<string, IPublicTypePreferenceValueType>>; + +export interface ILowCodePluginRuntimeCore { name: string; dep: string[]; disabled: boolean; - config: ILowCodePluginConfig; - logger: Logger; - on(event: string | symbol, listener: (...args: any[]) => void): any; - emit(event: string | symbol, ...args: any[]): boolean; - removeAllListeners(event?: string | symbol): this; + config: IPublicTypePluginConfig; + logger: IPublicApiLogger; + meta: IPublicTypePluginMeta; init(forceInit?: boolean): void; isInited(): boolean; destroy(): void; @@ -80,67 +39,64 @@ export interface ILowCodePluginCore { setDisabled(flag: boolean): void; } -interface ILowCodePluginExportsAccessor { +interface ILowCodePluginRuntimeExportsAccessor { [propName: string]: any; } -export type ILowCodePlugin = ILowCodePluginCore & ILowCodePluginExportsAccessor; - -export interface IDesignerCabin { - registerMetadataTransducer: (transducer: MetadataTransducer, level: number, id?: string) => void; - addBuiltinComponentAction: (action: ComponentAction) => void; - removeBuiltinComponentAction: (actionName: string) => void; +// eslint-disable-next-line max-len +export type ILowCodePluginRuntime = ILowCodePluginRuntimeCore & ILowCodePluginRuntimeExportsAccessor; + +export interface ILowCodePluginContextPrivate { + set hotkey(hotkey: IPublicApiHotkey); + set project(project: IPublicApiProject); + set skeleton(skeleton: IPublicApiSkeleton); + set setters(setters: IPublicApiSetters); + set material(material: IPublicApiMaterial); + set event(event: IPublicApiEvent); + set config(config: IPublicModelEngineConfig); + set common(common: IPublicApiCommon); + set plugins(plugins: IPublicApiPlugins); + set logger(plugins: IPublicApiLogger); + set pluginEvent(event: IPublicApiEvent); + set canvas(canvas: IPublicApiCanvas); + set workspace(workspace: IPublicApiWorkspace); + set editorWindow(window: IPublicModelWindow); + set registerLevel(level: IPublicEnumPluginRegisterLevel); + set isPluginRegisteredInWorkspace(flag: boolean); + set commonUI(commonUI: IPublicApiCommonUI); + set command(command: IPublicApiCommand); } - -export interface IPluginPreferenceMananger { - // eslint-disable-next-line max-len - getPreferenceValue: (key: string, defaultValue?: PreferenceValueType) => PreferenceValueType | undefined ; -} - -export interface ILowCodePluginContext { - skeleton: Skeleton; - hotkey: Hotkey; - logger: Logger; - plugins: ILowCodePluginManager; - setters: Setters; - config: EngineConfig; - material: Material; - event: Event; - project: Project; - preference: IPluginPreferenceMananger; +export interface ILowCodePluginContextApiAssembler { + assembleApis( + context: ILowCodePluginContextPrivate, + pluginName: string, + meta: IPublicTypePluginMeta, + ): void; } interface ILowCodePluginManagerPluginAccessor { - [pluginName: string]: ILowCodePlugin | any; + [pluginName: string]: ILowCodePluginRuntime | any; } export interface ILowCodePluginManagerCore { register( - pluginConfigCreator: (ctx: ILowCodePluginContext, pluginOptions?: any) => ILowCodePluginConfig, + pluginModel: IPublicTypePlugin, pluginOptions?: any, - options?: CompositeObject, + options?: IPublicTypePluginRegisterOptions, ): Promise<void>; - init(pluginPreference?: Map<string, Record<string, PreferenceValueType>>): Promise<void>; - get(pluginName: string): ILowCodePlugin | undefined; - getAll(): ILowCodePlugin[]; + init(pluginPreference?: Map<string, Record<string, IPublicTypePreferenceValueType>>): Promise<void>; + get(pluginName: string): ILowCodePluginRuntime | undefined; + getAll(): ILowCodePluginRuntime[]; has(pluginName: string): boolean; delete(pluginName: string): any; setDisabled(pluginName: string, flag: boolean): void; dispose(): void; + _getLowCodePluginContext (options: IPluginContextOptions): PluginContext; } export type ILowCodePluginManager = ILowCodePluginManagerCore & ILowCodePluginManagerPluginAccessor; -export function isLowCodeRegisterOptions(opts: any): opts is ILowCodeRegisterOptions { - return opts && ('autoInit' in opts || 'override' in opts); -} - -export interface ILowCodeRegisterOptions { - autoInit?: boolean; - // allow overriding existing plugin with same name when override === true - override?: boolean; -} - export interface IPluginContextOptions { pluginName: string; -} \ No newline at end of file + meta?: IPublicTypePluginMeta; +} diff --git a/packages/designer/src/plugin/plugin-utils.ts b/packages/designer/src/plugin/plugin-utils.ts index a929274009..7d8ab8db9c 100644 --- a/packages/designer/src/plugin/plugin-utils.ts +++ b/packages/designer/src/plugin/plugin-utils.ts @@ -1,9 +1,9 @@ -import type { ILowCodePluginPreferenceDeclaration } from './plugin-types'; import { isPlainObject } from 'lodash'; +import { IPublicTypePluginRegisterOptions, IPublicTypePluginDeclaration } from '@alilc/lowcode-types'; export function isValidPreferenceKey( key: string, - preferenceDeclaration: ILowCodePluginPreferenceDeclaration, + preferenceDeclaration: IPublicTypePluginDeclaration, ): boolean { if (!preferenceDeclaration || !Array.isArray(preferenceDeclaration.properties)) { return false; @@ -13,10 +13,17 @@ export function isValidPreferenceKey( }); } -export function filterValidOptions(opts: any, preferenceDeclaration: ILowCodePluginPreferenceDeclaration) { +export function isLowCodeRegisterOptions(opts: any): opts is IPublicTypePluginRegisterOptions { + return opts && ('autoInit' in opts || 'override' in opts); +} + +export function filterValidOptions( + opts: any, + preferenceDeclaration: IPublicTypePluginDeclaration, + ) { if (!opts || !isPlainObject(opts)) return opts; const filteredOpts = {} as any; - Object.keys(opts).forEach(key => { + Object.keys(opts).forEach((key) => { if (isValidPreferenceKey(key, preferenceDeclaration)) { const v = opts[key]; if (v !== undefined && v !== null) { diff --git a/packages/designer/src/plugin/plugin.ts b/packages/designer/src/plugin/plugin.ts index 0fd11ae9e4..dd57325fcd 100644 --- a/packages/designer/src/plugin/plugin.ts +++ b/packages/designer/src/plugin/plugin.ts @@ -1,27 +1,26 @@ import { getLogger, Logger } from '@alilc/lowcode-utils'; import { - ILowCodePlugin, - ILowCodePluginConfig, + ILowCodePluginRuntime, ILowCodePluginManager, - ILowCodePluginConfigMeta, } from './plugin-types'; -import { EventEmitter } from 'events'; +import { + IPublicTypePluginConfig, + IPublicTypePluginMeta, +} from '@alilc/lowcode-types'; import { invariant } from '../utils'; -export class LowCodePlugin implements ILowCodePlugin { - config: ILowCodePluginConfig; +export class LowCodePluginRuntime implements ILowCodePluginRuntime { + config: IPublicTypePluginConfig; logger: Logger; private manager: ILowCodePluginManager; - private emitter: EventEmitter; - private _inited: boolean; private pluginName: string; - private meta: ILowCodePluginConfigMeta; + meta: IPublicTypePluginMeta; /** * 标识插件状态,是否被 disabled @@ -31,15 +30,14 @@ export class LowCodePlugin implements ILowCodePlugin { constructor( pluginName: string, manager: ILowCodePluginManager, - config: ILowCodePluginConfig, - meta: ILowCodePluginConfigMeta, + config: IPublicTypePluginConfig, + meta: IPublicTypePluginMeta, ) { this.manager = manager; this.config = config; - this.emitter = new EventEmitter(); this.pluginName = pluginName; this.meta = meta; - this.logger = getLogger({ level: 'warn', bizName: `designer:plugin:${pluginName}` }); + this.logger = getLogger({ level: 'warn', bizName: `plugin:${pluginName}` }); } get name() { @@ -51,31 +49,17 @@ export class LowCodePlugin implements ILowCodePlugin { return [this.meta.dependencies]; } // compat legacy way to declare dependencies - if (typeof this.config.dep === 'string') { - return [this.config.dep]; + const legacyDepValue = (this.config as any).dep; + if (typeof legacyDepValue === 'string') { + return [legacyDepValue]; } - return this.meta.dependencies || this.config.dep || []; + return this.meta.dependencies || legacyDepValue || []; } get disabled() { return this._disabled; } - on(event: string | symbol, listener: (...args: any[]) => void): any { - this.emitter.on(event, listener); - return () => { - this.emitter.off(event, listener); - }; - } - - emit(event: string | symbol, ...args: any[]) { - return this.emitter.emit(event, ...args); - } - - removeAllListeners(event: string | symbol): any { - return this.emitter.removeAllListeners(event); - } - isInited() { return this._inited; } diff --git a/packages/designer/src/plugin/sequencify.ts b/packages/designer/src/plugin/sequencify.ts index a312df2075..9084b0a0e6 100644 --- a/packages/designer/src/plugin/sequencify.ts +++ b/packages/designer/src/plugin/sequencify.ts @@ -1,19 +1,50 @@ -function sequence(tasks, names, results, missing, recursive, nest) { +interface ITaks { + [key: string]: { + name: string; + dep: string[]; + }; +} + +export function sequence({ + tasks, + names, + results, + missing, + recursive, + nest, + parentName, +}: { + tasks: ITaks; + names: string[]; + results: string[]; + missing: string[]; + recursive: string[][]; + nest: string[]; + parentName: string; +}) { names.forEach((name) => { if (results.indexOf(name) !== -1) { return; // de-dup results } const node = tasks[name]; if (!node) { - missing.push(name); + missing.push([parentName, name].filter((d => !!d)).join('.')); } else if (nest.indexOf(name) > -1) { nest.push(name); recursive.push(nest.slice(0)); - nest.pop(name); + nest.pop(); } else if (node.dep.length) { nest.push(name); - sequence(tasks, node.dep, results, missing, recursive, nest); // recurse - nest.pop(name); + sequence({ + tasks, + parentName: name, + names: node.dep, + results, + missing, + recursive, + nest, + }); // recurse + nest.pop(); } results.push(name); }); @@ -21,12 +52,19 @@ function sequence(tasks, names, results, missing, recursive, nest) { // tasks: object with keys as task names // names: array of task names -export default function (tasks, names) { - let results = []; // the final sequence - const missing = []; // missing tasks - const recursive = []; // recursive task dependencies +export default function (tasks: ITaks, names: string[]) { + let results: string[] = []; // the final sequence + const missing: string[] = []; // missing tasks + const recursive: string[][] = []; // recursive task dependencies - sequence(tasks, names, results, missing, recursive, []); + sequence({ + tasks, + names, + results, + missing, + recursive, + nest: [], + }); if (missing.length || recursive.length) { results = []; // results are incomplete at best, completely wrong at worst, remove them to avoid confusion diff --git a/packages/designer/src/project/project-view.tsx b/packages/designer/src/project/project-view.tsx index c758893ff1..a16d4451a6 100644 --- a/packages/designer/src/project/project-view.tsx +++ b/packages/designer/src/project/project-view.tsx @@ -4,7 +4,7 @@ import { Designer } from '../designer'; import { BuiltinSimulatorHostView } from '../builtin-simulator'; import './project.less'; -class BuiltinLoading extends Component { +export class BuiltinLoading extends Component { render() { return ( <div id="engine-loading-wrapper"> @@ -26,8 +26,7 @@ export class ProjectView extends Component<{ designer: Designer }> { } render() { const { designer } = this.props; - const { project } = designer; - const { simulatorProps } = project; + const { project, projectSimulatorProps: simulatorProps } = designer; const Simulator = designer.simulatorComponent || BuiltinSimulatorHostView; const Loading = engineConfig.get('loadingComponent', BuiltinLoading); diff --git a/packages/designer/src/project/project.less b/packages/designer/src/project/project.less index 8d664a6c90..38f1f74428 100644 --- a/packages/designer/src/project/project.less +++ b/packages/designer/src/project/project.less @@ -13,6 +13,10 @@ padding-top: 50%; } + .lc-simulator { + background-color: var(--color-background, rgb(237, 239, 243)); + } + .lc-simulator-shell { width: 100%; height: 100%; diff --git a/packages/designer/src/project/project.ts b/packages/designer/src/project/project.ts index 726b0706f8..94913d9fa7 100644 --- a/packages/designer/src/project/project.ts +++ b/packages/designer/src/project/project.ts @@ -1,23 +1,101 @@ -import { EventEmitter } from 'events'; -import { obx, computed, makeObservable, action } from '@alilc/lowcode-editor-core'; -import { Designer } from '../designer'; -import { DocumentModel, isDocumentModel, isPageSchema } from '../document'; -import { - ProjectSchema, - RootSchema, - ComponentsMap, - TransformStage, - isLowCodeComponentType, - isProCodeComponentType, +import { obx, computed, makeObservable, action, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; +import { IDesigner } from '../designer'; +import { DocumentModel, isDocumentModel } from '../document'; +import type { IDocumentModel } from '../document'; +import { IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import type { + IBaseApiProject, + IPublicTypeProjectSchema, + IPublicTypeRootSchema, + IPublicTypeComponentsMap, + IPublicTypeSimulatorRenderer, } from '@alilc/lowcode-types'; +import { isLowCodeComponentType, isProCodeComponentType } from '@alilc/lowcode-utils'; import { ISimulatorHost } from '../simulator'; -export class Project { - private emitter = new EventEmitter(); +export interface IProject extends Omit<IBaseApiProject< + IDocumentModel +>, + 'simulatorHost' | + 'importSchema' | + 'exportSchema' | + 'openDocument' | + 'getDocumentById' | + 'getCurrentDocument' | + 'addPropsTransducer' | + 'onRemoveDocument' | + 'onChangeDocument' | + 'onSimulatorHostReady' | + 'onSimulatorRendererReady' | + 'setI18n' | + 'setConfig' | + 'currentDocument' | + 'selection' | + 'documents' | + 'createDocument' | + 'getDocumentByFileName' +> { + + get designer(): IDesigner; + + get simulator(): ISimulatorHost | null; + + get currentDocument(): IDocumentModel | null | undefined; + + get documents(): IDocumentModel[]; + + get i18n(): { + [local: string]: { + [key: string]: any; + }; + }; + + mountSimulator(simulator: ISimulatorHost): void; + + open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null; + + getDocumentByFileName(fileName: string): IDocumentModel | null; + + createDocument(data?: IPublicTypeRootSchema): IDocumentModel; + + load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string): void; + + getSchema( + stage?: IPublicEnumTransformStage, + ): IPublicTypeProjectSchema; + + getDocument(id: string): IDocumentModel | null; + + onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void; + + onSimulatorReady(fn: (args: any) => void): () => void; + + onRendererReady(fn: () => void): () => void; + + /** + * 分字段设置储存数据,不记录操作记录 + */ + set<T extends keyof IPublicTypeProjectSchema>(key: T, value: IPublicTypeProjectSchema[T]): void; + set(key: string, value: unknown): void; + + /** + * 分字段获取储存数据 + */ + get<T extends keyof IPublicTypeProjectSchema>(key: T): IPublicTypeProjectSchema[T]; + get<T>(key: string): T; + get(key: string): unknown; + + checkExclusive(activeDoc: DocumentModel): void; + + setRendererReady(renderer: IPublicTypeSimulatorRenderer<any, any>): void; +} + +export class Project implements IProject { + private emitter: IEventBus = createModuleEventBus('Project'); - @obx.shallow readonly documents: DocumentModel[] = []; + @obx.shallow readonly documents: IDocumentModel[] = []; - private data: ProjectSchema = { + private data: IPublicTypeProjectSchema = { version: '1.0.0', componentsMap: [], componentsTree: [], @@ -26,6 +104,8 @@ export class Project { private _simulator?: ISimulatorHost; + private isRendererReady: boolean = false; + /** * 模拟器 */ @@ -33,14 +113,7 @@ export class Project { return this._simulator || null; } - // TODO: 考虑项目级别 History - - constructor(readonly designer: Designer, schema?: ProjectSchema) { - makeObservable(this); - this.load(schema); - } - - @computed get currentDocument() { + @computed get currentDocument(): IDocumentModel | null | undefined { return this.documents.find((doc) => doc.active); } @@ -54,19 +127,29 @@ export class Project { } @obx.ref private _i18n: any = {}; - get i18n(): any { + @computed get i18n(): any { return this._i18n; } set i18n(value: any) { this._i18n = value || {}; } - private getComponentsMap(): ComponentsMap { - return this.documents.reduce((compomentsMap: ComponentsMap, curDoc: DocumentModel) => { + private documentsMap = new Map<string, DocumentModel>(); + + constructor(readonly designer: IDesigner, schema?: IPublicTypeProjectSchema, readonly viewName = 'global') { + makeObservable(this); + this.load(schema); + } + + private getComponentsMap(): IPublicTypeComponentsMap { + return this.documents.reduce<IPublicTypeComponentsMap>(( + componentsMap: IPublicTypeComponentsMap, + curDoc: IDocumentModel, + ): IPublicTypeComponentsMap => { const curComponentsMap = curDoc.getComponentsMap(); if (Array.isArray(curComponentsMap)) { curComponentsMap.forEach((item) => { - const found = compomentsMap.find((eItem) => { + const found = componentsMap.find((eItem) => { if ( isProCodeComponentType(eItem) && isProCodeComponentType(item) && @@ -83,35 +166,37 @@ export class Project { return false; }); if (found) return; - compomentsMap.push(item); + componentsMap.push(item); }); } - return compomentsMap; - }, [] as ComponentsMap); + return componentsMap; + }, [] as IPublicTypeComponentsMap); } /** * 获取项目整体 schema */ - getSchema(stage: TransformStage = TransformStage.Save): ProjectSchema { + getSchema( + stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Save, + ): IPublicTypeProjectSchema { return { ...this.data, componentsMap: this.getComponentsMap(), componentsTree: this.documents .filter((doc) => !doc.isBlank()) - .map((doc) => doc.export(stage)), + .map((doc) => doc.export(stage) || {} as IPublicTypeRootSchema), i18n: this.i18n, }; } /** - * 替换当前document的schema,并触发渲染器的render + * 替换当前 document 的 schema,并触发渲染器的 render * @param schema */ - setSchema(schema?: ProjectSchema) { + setSchema(schema?: IPublicTypeProjectSchema) { // FIXME: 这里的行为和 getSchema 并不对等,感觉不太对 const doc = this.documents.find((doc) => doc.active); - doc && doc.import(schema?.componentsTree[0]); + doc && schema?.componentsTree[0] && doc.import(schema?.componentsTree[0]); this.simulator?.rerender(); } @@ -121,7 +206,7 @@ export class Project { * @param autoOpen true 自动打开文档 string 指定打开的文件 */ @action - load(schema?: ProjectSchema, autoOpen?: boolean | string) { + load(schema?: IPublicTypeProjectSchema, autoOpen?: boolean | string) { this.unload(); // load new document this.data = { @@ -141,7 +226,7 @@ export class Project { const documentInstances = this.data.componentsTree.map((data) => this.createDocument(data)); // TODO: 暂时先读 config tabBar 里的值,后面看整个 layout 结构是否能作为引擎规范 if (this.config?.layout?.props?.tabBar?.items?.length > 0) { - // slice(1)这个贼不雅,默认任务fileName 是类'/fileName'的形式 + // slice(1) 这个贼不雅,默认任务 fileName 是类'/fileName'的形式 documentInstances .find((i) => i.fileName === this.config.layout.props.tabBar.items[0].path?.slice(1)) ?.open(); @@ -167,7 +252,7 @@ export class Project { } } - removeDocument(doc: DocumentModel) { + removeDocument(doc: IDocumentModel) { const index = this.documents.indexOf(doc); if (index < 0) { return; @@ -179,21 +264,9 @@ export class Project { /** * 分字段设置储存数据,不记录操作记录 */ - set( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - key: - | 'version' - | 'componentsTree' - | 'componentsMap' - | 'utils' - | 'constants' - | 'i18n' - | 'css' - | 'dataSource' - | string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - value: any, - ): void { + set<T extends keyof IPublicTypeProjectSchema>(key: T, value: IPublicTypeProjectSchema[T]): void; + set(key: string, value: unknown): void; + set(key: string, value: unknown): void { if (key === 'config') { this.config = value; } @@ -206,20 +279,10 @@ export class Project { /** * 分字段设置储存数据 */ - get( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - key: - | 'version' - | 'componentsTree' - | 'componentsMap' - | 'utils' - | 'constants' - | 'i18n' - | 'css' - | 'dataSource' - | 'config' - | string, - ): any { + get<T extends keyof IPublicTypeRootSchema>(key: T): IPublicTypeRootSchema[T]; + get<T>(key: string): T; + get(key: string): unknown; + get(key: string): any { if (key === 'config') { return this.config; } @@ -229,26 +292,24 @@ export class Project { return Reflect.get(this.data, key); } - private documentsMap = new Map<string, DocumentModel>(); - - getDocument(id: string): DocumentModel | null { + getDocument(id: string): IDocumentModel | null { // 此处不能使用 this.documentsMap.get(id),因为在乐高 rollback 场景,document.id 会被改成其他值 return this.documents.find((doc) => doc.id === id) || null; } - getDocumentByFileName(fileName: string): DocumentModel | null { + getDocumentByFileName(fileName: string): IDocumentModel | null { return this.documents.find((doc) => doc.fileName === fileName) || null; } @action - createDocument(data?: RootSchema): DocumentModel { + createDocument(data?: IPublicTypeRootSchema): IDocumentModel { const doc = new DocumentModel(this, data || this?.data?.componentsTree?.[0]); this.documents.push(doc); this.documentsMap.set(doc.id, doc); return doc; } - open(doc?: string | DocumentModel | RootSchema): DocumentModel | null { + open(doc?: string | IDocumentModel | IPublicTypeRootSchema): IDocumentModel | null { if (!doc) { const got = this.documents.find((item) => item.isBlank()); if (got) { @@ -257,13 +318,13 @@ export class Project { doc = this.createDocument(); return doc.open(); } - if (typeof doc === 'string') { - const got = this.documents.find((item) => item.fileName === doc || item.id === doc); + if (typeof doc === 'string' || typeof doc === 'number') { + const got = this.documents.find((item) => item.fileName === String(doc) || String(item.id) === String(doc)); if (got) { return got.open(); } - const data = this.data.componentsTree.find((data) => data.fileName === doc); + const data = this.data.componentsTree.find((data) => data.fileName === String(doc)); if (data) { doc = this.createDocument(data); return doc.open(); @@ -302,47 +363,39 @@ export class Project { }); } - /** - * 提供给模拟器的参数 - */ - @computed get simulatorProps(): object { - let { simulatorProps } = this.designer; - if (typeof simulatorProps === 'function') { - simulatorProps = simulatorProps(this); - } - return { - ...simulatorProps, - project: this, - onMount: this.mountSimulator.bind(this), - }; - } - - private mountSimulator(simulator: ISimulatorHost) { + mountSimulator(simulator: ISimulatorHost) { // TODO: 多设备 simulator 支持 this._simulator = simulator; - this.designer.editor.set('simulator', simulator); this.emitter.emit('lowcode_engine_simulator_ready', simulator); } setRendererReady(renderer: any) { + this.isRendererReady = true; this.emitter.emit('lowcode_engine_renderer_ready', renderer); } onSimulatorReady(fn: (args: any) => void): () => void { + if (this._simulator) { + fn(this._simulator); + return () => {}; + } this.emitter.on('lowcode_engine_simulator_ready', fn); return () => { this.emitter.removeListener('lowcode_engine_simulator_ready', fn); }; } - onRendererReady(fn: (args: any) => void): () => void { + onRendererReady(fn: () => void): () => void { + if (this.isRendererReady) { + fn(); + } this.emitter.on('lowcode_engine_renderer_ready', fn); return () => { this.emitter.removeListener('lowcode_engine_renderer_ready', fn); }; } - onCurrentDocumentChange(fn: (doc: DocumentModel) => void): () => void { + onCurrentDocumentChange(fn: (doc: IDocumentModel) => void): () => void { this.emitter.on('current-document.change', fn); return () => { this.emitter.removeListener('current-document.change', fn); diff --git a/packages/designer/src/simulator.ts b/packages/designer/src/simulator.ts index 5ba8ecda27..3a63a685b8 100644 --- a/packages/designer/src/simulator.ts +++ b/packages/designer/src/simulator.ts @@ -1,14 +1,18 @@ -import { Component as ReactComponent, ComponentType } from 'react'; -import { ComponentMetadata, NodeSchema } from '@alilc/lowcode-types'; -import { ISensor, Point, ScrollTarget, IScrollable, LocateEvent, LocationData } from './designer'; +import { ComponentType } from 'react'; +import { IPublicTypeComponentMetadata, IPublicTypeNodeSchema, IPublicTypeScrollable, IPublicTypeComponentInstance, IPublicModelSensor, IPublicTypeNodeInstance, IPublicTypePackage } from '@alilc/lowcode-types'; +import { Point, ScrollTarget, ILocateEvent, IDesigner } from './designer'; import { BuiltinSimulatorRenderer } from './builtin-simulator/renderer'; -import { Node, ParentalNode } from './document'; +import { INode } from './document'; +import { IProject } from './project'; export type AutoFit = '100%'; // eslint-disable-next-line no-redeclare export const AutoFit = '100%'; +export interface IScrollable extends IPublicTypeScrollable { +} export interface IViewport extends IScrollable { + /** * 视口大小 */ @@ -30,22 +34,27 @@ export interface IViewport extends IScrollable { * 视口矩形维度 */ readonly bounds: DOMRect; + /** * 内容矩形维度 */ readonly contentBounds: DOMRect; + /** * 视口滚动对象 */ readonly scrollTarget?: ScrollTarget; + /** * 是否滚动中 */ readonly scrolling: boolean; + /** * 内容当前滚动 X */ readonly scrollX: number; + /** * 内容当前滚动 Y */ @@ -63,15 +72,16 @@ export interface IViewport extends IScrollable { } export interface DropContainer { - container: ParentalNode; - instance: ComponentInstance; + container: INode; + instance: IPublicTypeComponentInstance; } /** * 模拟器控制进程协议 */ -export interface ISimulatorHost<P = object> extends ISensor { +export interface ISimulatorHost<P = object> extends IPublicModelSensor<INode> { readonly isSimulator: true; + /** * 获得边界维度等信息 */ @@ -80,6 +90,10 @@ export interface ISimulatorHost<P = object> extends ISensor { readonly contentDocument?: Document; readonly renderer?: BuiltinSimulatorRenderer; + readonly project: IProject; + + readonly designer: IDesigner; + // dependsAsset // like react jQuery lodash // themesAsset // componentsAsset @@ -88,7 +102,7 @@ export interface ISimulatorHost<P = object> extends ISensor { // // later: // layout: ComponentName - // 获取区块代码, 通过 components 传递,可异步获取 + // 获取区块代码,通过 components 传递,可异步获取 // 设置 simulator Props setProps(props: P): void; // 设置单个 Prop @@ -102,14 +116,17 @@ export interface ISimulatorHost<P = object> extends ISensor { * 设置文字拖选 */ setNativeSelection(enableFlag: boolean): void; + /** * 设置拖拽态 */ setDraggingState(state: boolean): void; + /** * 设置拷贝态 */ setCopyState(state: boolean): void; + /** * 清除所有态:拖拽态、拷贝态 */ @@ -120,70 +137,65 @@ export interface ISimulatorHost<P = object> extends ISensor { /** * 滚动视口到节点 */ - scrollToNode(node: Node, detail?: any): void; + scrollToNode(node: INode, detail?: any): void; /** * 描述组件 */ - generateComponentMetadata(componentName: string): ComponentMetadata; + generateComponentMetadata(componentName: string): IPublicTypeComponentMetadata; + /** * 根据组件信息获取组件类 */ getComponent(componentName: string): Component | any; + /** * 根据节点获取节点的组件实例 */ - getComponentInstances(node: Node): ComponentInstance[] | null; + getComponentInstances(node: INode): IPublicTypeComponentInstance[] | null; + /** * 根据 schema 创建组件类 */ - createComponent(schema: NodeSchema): Component | null; + createComponent(schema: IPublicTypeNodeSchema): Component | null; + /** * 根据节点获取节点的组件运行上下文 */ - getComponentContext(node: Node): object | null; + getComponentContext(node: INode): object | null; - getClosestNodeInstance(from: ComponentInstance, specId?: string): NodeInstance | null; + getClosestNodeInstance(from: IPublicTypeComponentInstance, specId?: string): IPublicTypeNodeInstance | null; - computeRect(node: Node): DOMRect | null; + computeRect(node: INode): DOMRect | null; - computeComponentInstanceRect(instance: ComponentInstance, selector?: string): DOMRect | null; + computeComponentInstanceRect(instance: IPublicTypeComponentInstance, selector?: string): DOMRect | null; - findDOMNodes(instance: ComponentInstance, selector?: string): Array<Element | Text> | null; + findDOMNodes(instance: IPublicTypeComponentInstance, selector?: string): Array<Element | Text> | null; - getDropContainer(e: LocateEvent): DropContainer | null; + getDropContainer(e: ILocateEvent): DropContainer | null; postEvent(evtName: string, evtData: any): void; rerender(): void; + /** * 销毁 */ purge(): void; + + setupComponents(library: IPublicTypePackage[]): Promise<void>; } export function isSimulatorHost(obj: any): obj is ISimulatorHost { return obj && obj.isSimulator; } -export interface NodeInstance<T = ComponentInstance> { - docId: string; - nodeId: string; - instance: T; - node?: Node | null; -} - /** * 组件类定义 */ export type Component = ComponentType<any> | object; -/** - * 组件实例定义 - */ -export type ComponentInstance = Element | ReactComponent<any> | object; - export interface INodeSelector { - node: Node; - instance?: ComponentInstance; + node: INode; + instance?: IPublicTypeComponentInstance; } diff --git a/packages/designer/src/transducers/index.ts b/packages/designer/src/transducers/index.ts index 43ee20dc57..48299f999c 100644 --- a/packages/designer/src/transducers/index.ts +++ b/packages/designer/src/transducers/index.ts @@ -1,4 +1,4 @@ -import { TransformedComponentMetadata as Metadata } from '@alilc/lowcode-types'; +import { IPublicTypeTransformedComponentMetadata as Metadata } from '@alilc/lowcode-types'; export function legacyIssues(metadata: Metadata): Metadata { const { devMode } = metadata; diff --git a/packages/designer/src/types/index.ts b/packages/designer/src/types/index.ts index 43b88bf285..50fd82bcd6 100644 --- a/packages/designer/src/types/index.ts +++ b/packages/designer/src/types/index.ts @@ -1,12 +1,4 @@ -import { getSetter, registerSetter, getSettersMap } from '@alilc/lowcode-editor-core'; -import { isFormEvent, compatibleLegaoSchema, getNodeSchemaById } from '@alilc/lowcode-utils'; -import { isNodeSchema } from '@alilc/lowcode-types'; - -export type Setters = { - getSetter: typeof getSetter; - registerSetter: typeof registerSetter; - getSettersMap: typeof getSettersMap; -}; +import { isFormEvent, compatibleLegaoSchema, getNodeSchemaById, isNodeSchema } from '@alilc/lowcode-utils'; export type NodeRemoveOptions = { suppressRemoveEvent?: boolean; @@ -18,21 +10,11 @@ export const utils = { compatibleLegaoSchema, getNodeSchemaById, }; -export type Utils = typeof utils; -export enum PROP_VALUE_CHANGED_TYPE { - /** - * normal set value - */ - SET_VALUE = 'SET_VALUE', - /** - * value changed caused by sub-prop value change - */ - SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE', +export enum EDITOR_EVENT { + NODE_CHILDREN_CHANGE = 'node.children.change', + + NODE_VISIBLE_CHANGE = 'node.visible.change', } -export interface ISetValueOptions { - disableMutator?: boolean; - type?: PROP_VALUE_CHANGED_TYPE; - fromSetHotValue?: boolean; -} \ No newline at end of file +export type Utils = typeof utils; \ No newline at end of file diff --git a/packages/designer/src/utils/invariant.ts b/packages/designer/src/utils/invariant.ts index e59a0342ee..b3c3b422b4 100644 --- a/packages/designer/src/utils/invariant.ts +++ b/packages/designer/src/utils/invariant.ts @@ -1,5 +1,5 @@ export function invariant(check: any, message: string, thing?: any) { if (!check) { - throw new Error(`[designer] Invariant failed: ${ message }${thing ? ` in '${thing}'` : ''}`); + throw new Error(`[designer] Invariant failed: ${message}${thing ? ` in '${thing}'` : ''}`); } } diff --git a/packages/designer/src/utils/slot.ts b/packages/designer/src/utils/slot.ts index 9061326b1b..09e90f7735 100644 --- a/packages/designer/src/utils/slot.ts +++ b/packages/designer/src/utils/slot.ts @@ -2,7 +2,7 @@ import { Node } from '../document/node/node'; export function includeSlot(node: Node, slotName: string | undefined): boolean { const { slots = [] } = node; - return slots.some(slot => { + return slots.some((slot) => { return slotName && slotName === slot?.getExtraProp('name')?.getAsString(); }); } diff --git a/packages/designer/tests/bugs/prop-variable-jse.test.ts b/packages/designer/tests/bugs/prop-variable-jse.test.ts index b39b5b54e7..0f32f0b57d 100644 --- a/packages/designer/tests/bugs/prop-variable-jse.test.ts +++ b/packages/designer/tests/bugs/prop-variable-jse.test.ts @@ -1,12 +1,12 @@ -// @ts-nocheck import { Editor } from '@alilc/lowcode-editor-core'; -import { isJSBlock, TransformStage } from '@alilc/lowcode-types'; -import { isPlainObject, isVariable } from '@alilc/lowcode-utils'; +import { IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import { isPlainObject, isVariable, isJSBlock } from '@alilc/lowcode-utils'; import '../fixtures/window'; import { Designer } from '../../src/designer/designer'; import { DocumentModel } from '../../src/document/document-model'; import { Project } from '../../src/project/project'; import formSchema from '../fixtures/schema/form'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; /** * bug 背景: @@ -58,8 +58,8 @@ describe('Node 方法测试', () => { it('原始 prop 值是 variable 结构,通过一个 propsReducer 转成了 JSExpression 结构', () => { editor = new Editor(); - designer = new Designer({ editor }); - designer.addPropsReducer(upgradePropsReducer, TransformStage.Upgrade); + designer = new Designer({ editor, shellModelFactory }); + designer.addPropsReducer(upgradePropsReducer, IPublicEnumTransformStage.Upgrade); project = designer.project; doc = new DocumentModel(project, formSchema); diff --git a/packages/designer/tests/builtin-simulator/bem-tools/drag-resize-engine.test.ts b/packages/designer/tests/builtin-simulator/bem-tools/drag-resize-engine.test.ts index 5f52e14b33..ccdc4b2b63 100644 --- a/packages/designer/tests/builtin-simulator/bem-tools/drag-resize-engine.test.ts +++ b/packages/designer/tests/builtin-simulator/bem-tools/drag-resize-engine.test.ts @@ -1,17 +1,12 @@ import '../../fixtures/window'; -import { set } from '../../utils'; import { Editor, globalContext } from '@alilc/lowcode-editor-core'; import { Project } from '../../../src/project/project'; import { DocumentModel } from '../../../src/document/document-model'; import { Designer } from '../../../src/designer/designer'; import DragResizeEngine from '../../../src/builtin-simulator/bem-tools/drag-resize-engine'; import formSchema from '../../fixtures/schema/form'; -import divMetadata from '../../fixtures/component-metadata/div'; -import formMetadata from '../../fixtures/component-metadata/form'; -import otherMeta from '../../fixtures/component-metadata/other'; -import pageMetadata from '../../fixtures/component-metadata/page'; import { fireEvent, createEvent } from '@testing-library/react'; -import { create } from 'lodash'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; describe('DragResizeEngine 测试', () => { let editor: Editor; @@ -26,7 +21,7 @@ describe('DragResizeEngine 测试', () => { }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = project.createDocument(formSchema); doc.open(); diff --git a/packages/designer/tests/builtin-simulator/bem-tools/manager.test.tsx b/packages/designer/tests/builtin-simulator/bem-tools/manager.test.tsx index 32e34020f7..bed1e27de9 100644 --- a/packages/designer/tests/builtin-simulator/bem-tools/manager.test.tsx +++ b/packages/designer/tests/builtin-simulator/bem-tools/manager.test.tsx @@ -1,21 +1,8 @@ import '../../fixtures/window'; -import { set, delayObxTick, delay } from '../../utils'; import { Editor } from '@alilc/lowcode-editor-core'; -import { Project } from '../../../src/project/project'; -import { DocumentModel } from '../../../src/document/document-model'; -import { - isRootNode, - Node, - isNode, - comparePosition, - contains, - insertChild, - insertChildren, - PositionNO, -} from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import { BemToolsManager } from '../../../src/builtin-simulator/bem-tools/manager'; -import formSchema from '../../fixtures/schema/form'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; describe('Node 方法测试', () => { let editor: Editor; @@ -26,7 +13,7 @@ describe('Node 方法测试', () => { beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); // project = designer.project; // doc = new DocumentModel(project, formSchema); manager = new BemToolsManager(designer); diff --git a/packages/designer/tests/builtin-simulator/host-view.test.tsx b/packages/designer/tests/builtin-simulator/host-view.test.tsx deleted file mode 100644 index ae3581df50..0000000000 --- a/packages/designer/tests/builtin-simulator/host-view.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; -import '../fixtures/window'; -import { Editor } from '@alilc/lowcode-editor-core'; -import { Project } from '../../src/project/project'; -import { Node } from '../../src/document/node/node'; -import TestRenderer from 'react-test-renderer'; -import { configure, render, mount } from 'enzyme'; -import Adapter from 'enzyme-adapter-react-16'; -import { Designer } from '../../src/designer/designer'; -import formSchema from '../fixtures/schema/form'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; -import { BuiltinSimulatorHostView } from '../../src/builtin-simulator/host-view'; - -configure({ adapter: new Adapter() }); -const editor = new Editor(); - -describe('host-view 测试', () => { - let designer: Designer; - beforeEach(() => { - designer = new Designer({ editor }); - }); - afterEach(() => { - designer._componentMetasMap.clear(); - designer = null; - }); - - it('host-view', () => { - const hostView = render(<BuiltinSimulatorHostView project={designer.project} />); - }); -}); diff --git a/packages/designer/tests/builtin-simulator/host.test.ts b/packages/designer/tests/builtin-simulator/host.test.ts index 746134c36b..d74c31d42c 100644 --- a/packages/designer/tests/builtin-simulator/host.test.ts +++ b/packages/designer/tests/builtin-simulator/host.test.ts @@ -1,36 +1,32 @@ -// @ts-ignore -import React from 'react'; -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; +import { IPublicTypePluginMeta } from './../../../../lib/packages/types/src/shell/type/plugin-meta.d'; import '../fixtures/window'; -import { Editor, globalContext } from '@alilc/lowcode-editor-core'; import { - AssetLevel, - Asset, - AssetList, - assetBundle, - assetItem, + Editor, + globalContext, + Hotkey as InnerHotkey, + Setters as InnerSetters, +} from '@alilc/lowcode-editor-core'; +import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace'; +import { AssetType, } from '@alilc/lowcode-utils'; import { - Dragon, - isDragNodeObject, - isDragNodeDataObject, - isDragAnyObject, - isLocateEvent, - DragObjectType, - isShaken, - setShaken, -} from '../../src/designer/dragon'; + IPublicEnumDragObjectType, +} from '@alilc/lowcode-types'; import { Project } from '../../src/project/project'; import pageMetadata from '../fixtures/component-metadata/page'; -import { Node } from '../../src/document/node/node'; import { Designer } from '../../src/designer/designer'; import { DocumentModel } from '../../src/document/document-model'; import formSchema from '../fixtures/schema/form'; import { getMockDocument, getMockWindow, getMockEvent, delayObxTick } from '../utils'; import { BuiltinSimulatorHost } from '../../src/builtin-simulator/host'; import { fireEvent } from '@testing-library/react'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; +import { Setters, Workspace } from '@alilc/lowcode-shell'; +import { ILowCodePluginContextApiAssembler, ILowCodePluginContextPrivate, LowCodePluginManager } from '@alilc/lowcode-designer'; +import { + Skeleton as InnerSkeleton, +} from '@alilc/lowcode-editor-skeleton'; describe('Host 测试', () => { let editor: Editor; @@ -41,15 +37,32 @@ describe('Host 测试', () => { beforeAll(() => { editor = new Editor(); + const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + assembleApis: (context: ILowCodePluginContextPrivate, pluginName: string, meta: IPublicTypePluginMeta) => { + context.project = project; + const eventPrefix = meta?.eventPrefix || 'common'; + context.workspace = workspace; + }, + }; + const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler); + const innerWorkspace = new InnerWorkspace(() => {}, {}); + const workspace = new Workspace(innerWorkspace); + const innerSkeleton = new InnerSkeleton(editor); + editor.set('skeleton' as any, innerSkeleton); + editor.set('innerHotkey', new InnerHotkey()) + editor.set('setters', new Setters(new InnerSetters())); + editor.set('innerPlugins' as any, innerPlugins); !globalContext.has(Editor) && globalContext.register(editor, Editor); + !globalContext.has('workspace') && globalContext.register(innerWorkspace, 'workspace'); }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; designer.createComponentMeta(pageMetadata); doc = project.createDocument(formSchema); - host = new BuiltinSimulatorHost(designer.project); + host = new BuiltinSimulatorHost(designer.project, designer); }); afterEach(() => { @@ -266,7 +279,7 @@ describe('Host 测试', () => { host.getDropContainer({ target: {}, dragObject: { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('page')], }, }); @@ -361,7 +374,7 @@ describe('Host 测试', () => { it('locate,没有 nodes', () => { expect(host.locate({ dragObject: { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [], }, })).toBeUndefined(); @@ -370,7 +383,7 @@ describe('Host 测试', () => { project.removeDocument(doc); expect(host.locate({ dragObject: { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('page')], }, })).toBeNull(); @@ -378,7 +391,7 @@ describe('Host 测试', () => { it('notFoundComponent', () => { expect(host.locate({ dragObject: { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('form')], }, })).toBeUndefined(); @@ -386,7 +399,7 @@ describe('Host 测试', () => { it('locate', () => { host.locate({ dragObject: { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('page')], }, }); 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 f2f399c96e..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,9 +1,177 @@ 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 () => { const md1 = parseMetadata('Div'); const md2 = parseMetadata({ componentName: 'Div' }); }); + it('LowcodeTypes.shape', async () => { + const result = (window as any).PropTypes.shape() + 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/builtin-simulator/viewport.test.ts b/packages/designer/tests/builtin-simulator/viewport.test.ts index 2938e008a1..e9972fc7c3 100644 --- a/packages/designer/tests/builtin-simulator/viewport.test.ts +++ b/packages/designer/tests/builtin-simulator/viewport.test.ts @@ -1,11 +1,11 @@ import '../fixtures/window'; -import { getMockWindow, set, getMockElement, delay } from '../utils'; +import { getMockWindow, getMockElement, delay } from '../utils'; import { Editor, globalContext } from '@alilc/lowcode-editor-core'; import { Project } from '../../src/project/project'; import { DocumentModel } from '../../src/document/document-model'; import Viewport from '../../src/builtin-simulator/viewport'; import { Designer } from '../../src/designer/designer'; -import { fireEvent } from '@testing-library/react'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; describe('Viewport 测试', () => { @@ -28,7 +28,7 @@ describe('Viewport 测试', () => { }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; // doc = project.createDocument(formSchema); }); diff --git a/packages/designer/tests/designer/builtin-hotkey.test.ts b/packages/designer/tests/designer/builtin-hotkey.test.ts index d0fa8a47f8..9cb068ac19 100644 --- a/packages/designer/tests/designer/builtin-hotkey.test.ts +++ b/packages/designer/tests/designer/builtin-hotkey.test.ts @@ -1,31 +1,60 @@ import '../fixtures/window'; -import { Editor, globalContext } from '@alilc/lowcode-editor-core'; +import { + Editor, + globalContext, + Hotkey as InnerHotkey, +} from '@alilc/lowcode-editor-core'; import { Designer } from '../../src/designer/designer'; import formSchema from '../fixtures/schema/form'; -import '../../src/designer/builtin-hotkey'; import { fireEvent } from '@testing-library/react'; -import { isInLiveEditing } from '../../src/designer/builtin-hotkey'; +import { builtinHotkey } from '../../../engine/src/inner-plugins/builtin-hotkey'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; +import { ILowCodePluginContextPrivate, LowCodePluginManager } from '@alilc/lowcode-designer'; +import { IPublicApiPlugins } from '@alilc/lowcode-types'; +import { Logger, Project, Canvas } from '@alilc/lowcode-shell'; +import { Workspace } from '@alilc/lowcode-workspace'; const editor = new Editor(); +const workspace = new Workspace(); let designer: Designer; -describe('error scenarios', () => { - it('edtior not registered', () => { - expect(isInLiveEditing()).toBeUndefined(); - }); -}); - // keyCode 对应表:https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode // hotkey 模块底层用的 keyCode,所以还不能用 key / code 测试 describe('快捷键测试', () => { + let pluginManager: LowCodePluginManager; + let project: any = {}; beforeAll(() => { - globalContext.register(editor, Editor); + return new Promise((resolve, reject) => { + const hotkey: any = new InnerHotkey(); + const logger = new Logger({ level: 'warn', bizName: 'common' }); + const contextApiAssembler = { + assembleApis(context: ILowCodePluginContextPrivate){ + context.plugins = pluginManager as IPublicApiPlugins; + context.hotkey = hotkey; + context.logger = logger; + context.project = project; + context.canvas = new Canvas(editor); + } + }; + pluginManager = new LowCodePluginManager(contextApiAssembler).toProxy(); + pluginManager.register(builtinHotkey); + globalContext.register(editor, Editor); + globalContext.register(editor, 'editor'); + globalContext.register(workspace, 'workspace'); + pluginManager.init().then(() => { + resolve({}); + }); + }) + }); + afterAll(() => { + pluginManager.dispose(); }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); editor.set('designer', designer); designer.project.open(formSchema); + project.__proto__ = new Project(designer.project); }); afterEach(() => { designer = null; diff --git a/packages/designer/tests/designer/designer.test.ts b/packages/designer/tests/designer/designer.test.ts index 51fbabea7e..8bca7d84a1 100644 --- a/packages/designer/tests/designer/designer.test.ts +++ b/packages/designer/tests/designer/designer.test.ts @@ -1,16 +1,18 @@ import '../fixtures/window'; -import { Editor, globalContext } from '@alilc/lowcode-editor-core'; +import { Editor, globalContext, Setters } from '@alilc/lowcode-editor-core'; import { Project } from '../../src/project/project'; import { DocumentModel } from '../../src/document/document-model'; import { Designer } from '../../src/designer/designer'; -import { Dragon, DragObjectType } from '../../src/designer/dragon'; -import { TransformStage } from '../../src/document/node/transform-stage'; +import { Dragon } from '../../src/designer/dragon'; +// import { TransformStage } from '../../src/document/node/transform-stage'; import formSchema from '../fixtures/schema/form'; import buttonMetadata from '../fixtures/component-metadata/button'; import pageMetadata from '../fixtures/component-metadata/page'; import divMetadata from '../fixtures/component-metadata/div'; import { delayObxTick } from '../utils'; import { fireEvent } from '@testing-library/react'; +import { IPublicEnumDragObjectType, IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; const mockNode = { internalToShellNode() { @@ -27,11 +29,13 @@ describe('Designer 测试', () => { beforeAll(() => { editor = new Editor(); + const setters = new Setters(); + editor.set('setters', setters); !globalContext.has(Editor) && globalContext.register(editor, Editor); }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = project.createDocument(formSchema); dragon = new Dragon(designer); @@ -47,7 +51,7 @@ describe('Designer 测试', () => { }); describe('onDragstart / onDrag / onDragend', () => { - it('DragObjectType.Node', () => { + it('IPublicEnumDragObjectType.Node', () => { const dragStartMockFn = jest.fn(); const dragMockFn = jest.fn(); const dragEndMockFn = jest.fn(); @@ -57,6 +61,7 @@ describe('Designer 测试', () => { const designer = new Designer({ editor, + shellModelFactory, onDragstart: dragStartMockFn, onDrag: dragMockFn, onDragend: dragEndMockFn, @@ -68,7 +73,7 @@ describe('Designer 测试', () => { dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -98,6 +103,7 @@ describe('Designer 测试', () => { return x; }, insert() {}, + internalInsert() {}, }, }; const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } }; @@ -113,7 +119,7 @@ describe('Designer 测试', () => { } }); - it('DragObjectType.NodeData', () => { + it('IPublicEnumDragObjectType.NodeData', () => { const dragStartMockFn = jest.fn(); const dragMockFn = jest.fn(); const dragEndMockFn = jest.fn(); @@ -123,6 +129,7 @@ describe('Designer 测试', () => { const designer = new Designer({ editor, + shellModelFactory, onDragstart: dragStartMockFn, onDrag: dragMockFn, onDragend: dragEndMockFn, @@ -134,7 +141,7 @@ describe('Designer 测试', () => { dragon.boost( { - type: DragObjectType.NodeData, + type: IPublicEnumDragObjectType.NodeData, data: [{ componentName: 'Button', }], @@ -166,6 +173,7 @@ describe('Designer 测试', () => { return x; }, insert() {}, + internalInsert() {}, }, }; const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } }; @@ -184,56 +192,56 @@ describe('Designer 测试', () => { it('addPropsReducer / transformProps', () => { // 没有相应的 reducer - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Init)).toEqual({ num: 1 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({ num: 1 }); // props 是数组 - expect(designer.transformProps([{ num: 1 }], mockNode, TransformStage.Init)).toEqual([{ num: 1 }]); + expect(designer.transformProps([{ num: 1 }], mockNode, IPublicEnumTransformStage.Init)).toEqual([{ num: 1 }]); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Init); + }, IPublicEnumTransformStage.Init); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Init); + }, IPublicEnumTransformStage.Init); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Clone); + }, IPublicEnumTransformStage.Clone); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Serilize); + }, IPublicEnumTransformStage.Serilize); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Render); + }, IPublicEnumTransformStage.Render); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Save); + }, IPublicEnumTransformStage.Save); designer.addPropsReducer((props, node) => { props.num += 1; return props; - }, TransformStage.Upgrade); + }, IPublicEnumTransformStage.Upgrade); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Init)).toEqual({ num: 3 }); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Clone)).toEqual({ num: 2 }); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Serilize)).toEqual({ num: 2 }); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Render)).toEqual({ num: 2 }); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Save)).toEqual({ num: 2 }); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Upgrade)).toEqual({ num: 2 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Init)).toEqual({ num: 3 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Clone)).toEqual({ num: 2 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Serilize)).toEqual({ num: 2 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Render)).toEqual({ num: 2 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Save)).toEqual({ num: 2 }); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade)).toEqual({ num: 2 }); designer.addPropsReducer((props, node) => { throw new Error('calculate error'); - }, TransformStage.Upgrade); - expect(designer.transformProps({ num: 1 }, mockNode, TransformStage.Upgrade)).toEqual({ num: 2 }); + }, IPublicEnumTransformStage.Upgrade); + expect(designer.transformProps({ num: 1 }, mockNode, IPublicEnumTransformStage.Upgrade)).toEqual({ num: 2 }); }); it('setProps', () => { @@ -244,14 +252,18 @@ describe('Designer 测试', () => { suspensed: true, componentMetadatas: [buttonMetadata, divMetadata], }; - designer = new Designer({ editor, ...initialProps }); + designer = new Designer({ + editor, + shellModelFactory, + ...initialProps, + }); expect(designer.simulatorComponent).toEqual({ isSimulatorComp: true }); expect(designer.simulatorProps).toEqual({ designMode: 'design' }); expect(designer.suspensed).toBeTruthy(); - expect(designer._componentMetasMap.has('Div')).toBeTruthy(); - expect(designer._componentMetasMap.has('Button')).toBeTruthy(); - const { editor: editorFromDesigner, ...others } = designer.props; + expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy(); + expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy(); + const { editor: editorFromDesigner, shellModelFactory: shellModelFactoryFromDesigner, ...others } = (designer as any).props; expect(others).toEqual(initialProps); expect(designer.get('simulatorProps')).toEqual({ designMode: 'design' }); expect(designer.get('suspensed')).toBeTruthy(); @@ -269,9 +281,9 @@ describe('Designer 测试', () => { expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true }); expect(designer.simulatorProps).toEqual({ designMode: 'live' }); expect(designer.suspensed).toBeFalsy(); - expect(designer._componentMetasMap.has('Button')).toBeTruthy(); - expect(designer._componentMetasMap.has('Div')).toBeTruthy(); - const { editor: editorFromDesigner2, ...others2 } = designer.props; + expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy(); + expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy(); + const { editor: editorFromDesigner2, shellModelFactory: shellModelFactoryFromDesigner2, ...others2 } = (designer as any).props; expect(others2).toEqual(updatedProps); // 第三次设置 props,跟第二次值一样,for 覆盖率测试 @@ -281,9 +293,9 @@ describe('Designer 测试', () => { expect(designer.simulatorComponent).toEqual({ isSimulatorComp2: true }); expect(designer.simulatorProps).toEqual({ designMode: 'live' }); expect(designer.suspensed).toBeFalsy(); - expect(designer._componentMetasMap.has('Button')).toBeTruthy(); - expect(designer._componentMetasMap.has('Div')).toBeTruthy(); - const { editor: editorFromDesigner3, ...others3 } = designer.props; + expect((designer as any)._componentMetasMap.has('Button')).toBeTruthy(); + expect((designer as any)._componentMetasMap.has('Div')).toBeTruthy(); + const { editor: editorFromDesigner3, shellModelFactory: shellModelFactoryFromDesigner3, ...others3 } = (designer as any).props; expect(others3).toEqual(updatedProps); }); @@ -397,6 +409,7 @@ describe('Designer 测试', () => { return x; }, insert() {}, + internalInsert() {}, }, }; const mockDetail = { type: 'Children', index: 1, near: { node: { x: 1 } } }; @@ -421,6 +434,7 @@ describe('Designer 测试', () => { return x; }, insert() {}, + internalInsert() {}, }, }, detail: mockDetail, diff --git a/packages/designer/tests/designer/dragon.test.ts b/packages/designer/tests/designer/dragon.test.ts index eaa53bd5b5..1041f0425d 100644 --- a/packages/designer/tests/designer/dragon.test.ts +++ b/packages/designer/tests/designer/dragon.test.ts @@ -1,5 +1,4 @@ import '../fixtures/window'; -import { set } from '../utils'; import { Editor, globalContext } from '@alilc/lowcode-editor-core'; import { Project } from '../../src/project/project'; import { DocumentModel } from '../../src/document/document-model'; @@ -10,14 +9,15 @@ import { isDragNodeDataObject, isDragAnyObject, isLocateEvent, - DragObjectType, isShaken, setShaken, isInvalidPoint, isSameAs, } from '../../src/designer/dragon'; +import { IPublicEnumDragObjectType } from '@alilc/lowcode-types'; import formSchema from '../fixtures/schema/form'; import { fireEvent } from '@testing-library/react'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; describe('Dragon 测试', () => { let editor: Editor; @@ -32,7 +32,7 @@ describe('Dragon 测试', () => { }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = project.createDocument(formSchema); dragon = new Dragon(designer); @@ -66,7 +66,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.NodeData, + type: IPublicEnumDragObjectType.NodeData, data: [{ componentName: 'Button' }], }, new Event('dragstart', { clientX: 100, clientY: 100 }), @@ -97,7 +97,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.NodeData, + type: IPublicEnumDragObjectType.NodeData, data: [{ componentName: 'Button' }], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -123,7 +123,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -150,7 +150,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -172,7 +172,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -193,7 +193,7 @@ describe('Dragon 测试', () => { dragon.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -217,7 +217,7 @@ describe('Dragon 测试', () => { const mockBoostFn = jest .fn((e) => { return { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }; }) @@ -274,7 +274,7 @@ describe('Dragon 测试', () => { expect(dragon.activeSensor).toBeUndefined(); dragon.boost( { - type: DragObjectType.NodeData, + type: IPublicEnumDragObjectType.NodeData, data: [{ componentName: 'Button' }], }, new MouseEvent('mousedown', { clientX: 100, clientY: 100 }), @@ -309,7 +309,7 @@ describe('Dragon 测试', () => { const mockBoostFn = jest .fn((e) => { return { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes: [doc.getNode('node_k1ow3cbn')], }; }) @@ -324,15 +324,15 @@ describe('Dragon 测试', () => { describe('导出的其他函数', () => { it('isDragNodeObject', () => { - expect(isDragNodeObject({ type: DragObjectType.Node, nodes: [] })).toBeTruthy(); + expect(isDragNodeObject({ type: IPublicEnumDragObjectType.Node, nodes: [] })).toBeTruthy(); }); it('isDragNodeDataObject', () => { - expect(isDragNodeDataObject({ type: DragObjectType.NodeData, data: [] })).toBeTruthy(); + expect(isDragNodeDataObject({ type: IPublicEnumDragObjectType.NodeData, data: [] })).toBeTruthy(); }); it('isDragAnyObject', () => { expect(isDragAnyObject()).toBeFalsy(); - expect(isDragAnyObject({ type: DragObjectType.Node, nodes: [] })).toBeFalsy(); - expect(isDragAnyObject({ type: DragObjectType.NodeData, data: [] })).toBeFalsy(); + expect(isDragAnyObject({ type: IPublicEnumDragObjectType.Node, nodes: [] })).toBeFalsy(); + expect(isDragAnyObject({ type: IPublicEnumDragObjectType.NodeData, data: [] })).toBeFalsy(); expect(isDragAnyObject({ type: 'others', data: [] })).toBeTruthy(); }); it('isLocateEvent', () => { diff --git a/packages/designer/tests/designer/scroller.test.ts b/packages/designer/tests/designer/scroller.test.ts index 00f0b86e89..ff03608b04 100644 --- a/packages/designer/tests/designer/scroller.test.ts +++ b/packages/designer/tests/designer/scroller.test.ts @@ -1,35 +1,14 @@ import '../fixtures/window'; -import { set } from '../utils'; import { Editor, globalContext } from '@alilc/lowcode-editor-core'; import { Project } from '../../src/project/project'; import { DocumentModel } from '../../src/document/document-model'; import { ScrollTarget, Scroller } from '../../src/designer/scroller'; -import { - isRootNode, - isNode, - comparePosition, - contains, - insertChild, - insertChildren, - PositionNO, -} from '../../src/document/node/node'; import { Designer } from '../../src/designer/designer'; import { Dragon, - isDragNodeObject, - isDragNodeDataObject, - isDragAnyObject, - isLocateEvent, - DragObjectType, - isShaken, - setShaken, } from '../../src/designer/dragon'; import formSchema from '../fixtures/schema/form'; -import divMetadata from '../fixtures/component-metadata/div'; -import formMetadata from '../fixtures/component-metadata/form'; -import otherMeta from '../fixtures/component-metadata/other'; -import pageMetadata from '../fixtures/component-metadata/page'; -import { fireEvent } from '@testing-library/react'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; describe('Scroller 测试', () => { let editor: Editor; @@ -44,7 +23,7 @@ describe('Scroller 测试', () => { }); beforeEach(() => { - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = project.createDocument(formSchema); dragon = new Dragon(designer); diff --git a/packages/designer/tests/designer/setting/setting-field.test.ts b/packages/designer/tests/designer/setting/setting-field.test.ts index 0e600e2eaf..53ed2829df 100644 --- a/packages/designer/tests/designer/setting/setting-field.test.ts +++ b/packages/designer/tests/designer/setting/setting-field.test.ts @@ -1,7 +1,12 @@ // @ts-nocheck import '../../fixtures/window'; -import { Editor } from '@alilc/lowcode-editor-core'; -import { Project } from '../../../src/project/project'; +import { + Editor, + Setters as InnerSetters, +} from '@alilc/lowcode-editor-core'; +import { + Setters, +} from '@alilc/lowcode-shell'; import { SettingTopEntry } from '../../../src/designer/setting/setting-top-entry'; import { SettingField } from '../../../src/designer/setting/setting-field'; import { Node } from '../../../src/document/node/node'; @@ -10,14 +15,18 @@ import settingSchema from '../../fixtures/schema/setting'; import buttonMeta from '../../fixtures/component-metadata/button'; import { DocumentModel } from 'designer/src/document'; import { delayObxTick } from '../../utils'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; const editor = new Editor(); describe('setting-field 测试', () => { let designer: Designer; let doc: DocumentModel; + let setters: Setters; beforeEach(() => { - designer = new Designer({ editor }); + setters = new InnerSetters(); + editor.set('setters', setters); + designer = new Designer({ editor, shellModelFactory }); designer.createComponentMeta(buttonMeta); doc = designer.project.open(settingSchema); }); @@ -56,8 +65,8 @@ describe('setting-field 测试', () => { it('常规方法', () => { // 普通 field - const settingEntry = mockNode.settingEntry as SettingTopEntry; - const field = settingEntry.get('behavior') as SettingField; + const settingEntry = mockNode.settingEntry; + const field = settingEntry.get('behavior'); expect(field.title).toBe('默认状态'); expect(field.expanded).toBeTruthy(); field.setExpanded(false); @@ -94,24 +103,24 @@ describe('setting-field 测试', () => { expect(nonExistingField.setter).toBeNull(); // group 类型的 field - const groupField = settingEntry.get('groupkgzzeo41') as SettingField; - expect(groupField.items).toBeUndefined(); + const groupField = settingEntry.get('groupkgzzeo41'); + expect(groupField.items).toEqual([]); // 有子节点的 field - const objField = settingEntry.get('obj') as SettingField; + const objField = settingEntry.get('obj'); expect(objField.items).toHaveLength(3); expect(objField.getItems()).toHaveLength(3); expect(objField.getItems(x => x.name === 'a')).toHaveLength(1); objField.purge(); expect(objField.items).toHaveLength(0); - const objAField = settingEntry.get('obj.a') as SettingField; + const objAField = settingEntry.get('obj.a'); expect(objAField.setter).toBe('StringSetter'); }); it('setValue / getValue / setHotValue / getHotValue', () => { // 获取已有的 prop const settingEntry = mockNode.settingEntry as SettingTopEntry; - const field = settingEntry.get('behavior') as SettingField; + const field = settingEntry.get('behavior'); // 会读取 extraProps.defaultValue expect(field.getHotValue()).toBe('NORMAL'); @@ -131,11 +140,71 @@ describe('setting-field 测试', () => { // dirty fix list setter field.setHotValue([{ __sid__: 1 }]); + + // 数组的 field + const arrField = settingEntry.get('arr'); + const subArrField = arrField.createField({ + name: 0, + title: 'sub', + }); + const subArrField02 = arrField.createField({ + name: 1, + title: 'sub', + }); + const subArrField03 = arrField.createField({ + name: '2', + title: 'sub', + }); + subArrField.setValue({name: '1'}); + expect(subArrField.path).toEqual(['arr', 0]); + expect(subArrField02.path).toEqual(['arr', 1]); + subArrField02.setValue({name: '2'}); + expect(subArrField.getValue()).toEqual({name: '1'}); + expect(arrField.getHotValue()).toEqual([{name: '1'}, {name: '2'}]); + subArrField.clearValue(); + expect(subArrField.getValue()).toBeUndefined(); + expect(arrField.getHotValue()).toEqual([undefined, {name: '2'}]); + subArrField03.setValue({name: '3'}); + expect(arrField.getHotValue()).toEqual([undefined, {name: '2'}, {name: '3'}]); + }); + + it('js expression setValue / setHotValue', () => { + const settingEntry = mockNode.settingEntry; + const field = settingEntry.get('behavior'); + + const subField = field.createField({ + name: 'sub', + title: 'sub', + }); + subField.setValue({ + type: 'JSExpression', + value: 'state.a', + mock: 'haha', + }); + + subField.setHotValue({ + type: 'JSExpression', + value: 'state.b', + }); + + expect(subField.getValue()).toEqual({ + type: 'JSExpression', + value: 'state.b', + mock: 'haha', + }); + + subField.setHotValue('mock02'); + + expect(subField.getValue()).toEqual({ + type: 'JSExpression', + value: 'state.b', + mock: 'mock02', + }); }); it('onEffect', async () => { const settingEntry = mockNode.settingEntry as SettingTopEntry; - const field = settingEntry.get('behavior') as SettingField; + const field = settingEntry.get('behavior'); const mockFn = jest.fn(); @@ -147,5 +216,66 @@ describe('setting-field 测试', () => { expect(mockFn).toHaveBeenCalled(); }); + + it('autorun', async () => { + const settingEntry = mockNode.settingEntry as SettingTopEntry; + const arrField = settingEntry.get('columns'); + const subArrField = arrField.createField({ + name: 0, + title: 'sub', + }); + const objSubField = subArrField.createField({ + name: 'objSub', + title: 'objSub', + }); + const mockFnArrField = jest.fn(); + const mockFnSubArrField = jest.fn(); + const mockFnObjSubField = jest.fn(); + + arrField.setValue([{ objSub: "subMock0.Index.0" }]); + // 这里需要 setValue 两遍,触发 prop 的 purge 方法,使 purged 为 true,之后的 purge 方法不会正常执行,prop 才能正常缓存,autorun 才能正常执行 + // TODO: 该机制后续得研究一下,再确定是否要修改 + arrField.setValue([{ objSub: "subMock0.Index.0" }]); + + arrField.onEffect(() => { + mockFnArrField(arrField.getValue()); + }); + arrField.onEffect(() => { + mockFnSubArrField(subArrField.getValue()); + }); + arrField.onEffect(() => { + mockFnObjSubField(objSubField.getValue()); + }); + + await delayObxTick(); + + expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.0'); + expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.0" }); + expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.0" }]); + + arrField.setValue([{ objSub: "subMock0.Index.1" }]); + + await delayObxTick(); + + expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.1'); + expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.1" }); + expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.1" }]); + + subArrField.setValue({ objSub: "subMock0.Index.2" }); + + await delayObxTick(); + + expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.2'); + expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.2" }); + expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.2" }]); + + objSubField.setValue('subMock0.Index.3'); + + await delayObxTick(); + + expect(mockFnObjSubField).toHaveBeenCalledWith('subMock0.Index.3'); + expect(mockFnSubArrField).toHaveBeenCalledWith({ objSub: "subMock0.Index.3" }); + expect(mockFnArrField).toHaveBeenCalledWith([{ objSub: "subMock0.Index.3" }]); + }) }); }); diff --git a/packages/designer/tests/designer/setting/setting-prop-entry.test.ts b/packages/designer/tests/designer/setting/setting-prop-entry.test.ts index 72b3940355..3ece67af76 100644 --- a/packages/designer/tests/designer/setting/setting-prop-entry.test.ts +++ b/packages/designer/tests/designer/setting/setting-prop-entry.test.ts @@ -1,26 +1,27 @@ -// @ts-nocheck -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; -import { Editor } from '@alilc/lowcode-editor-core'; -import { Project } from '../../../src/project/project'; +import { + Editor, + Setters as InnerSetters, +} from '@alilc/lowcode-editor-core'; import { SettingTopEntry } from '../../../src/designer/setting/setting-top-entry'; import { SettingPropEntry } from '../../../src/designer/setting/setting-prop-entry'; import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; -import formSchema from '../../../fixtures/schema/form'; import settingSchema from '../../fixtures/schema/setting'; import divMeta from '../../fixtures/component-metadata/div'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; import { DocumentModel } from 'designer/src/document'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; const editor = new Editor(); describe('setting-prop-entry 测试', () => { let designer: Designer; let doc: DocumentModel; + let setters: any; beforeEach(() => { - designer = new Designer({ editor }); + setters = new InnerSetters(); + editor.set('setters', setters); + designer = new Designer({ editor, shellModelFactory }); designer.createComponentMeta(divMeta); doc = designer.project.open(settingSchema); }); diff --git a/packages/designer/tests/designer/setting/setting-top-entry.test.ts b/packages/designer/tests/designer/setting/setting-top-entry.test.ts index e7a2a7a662..23a42c2afc 100644 --- a/packages/designer/tests/designer/setting/setting-top-entry.test.ts +++ b/packages/designer/tests/designer/setting/setting-top-entry.test.ts @@ -1,22 +1,18 @@ -// @ts-nocheck -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; -import { Editor } from '@alilc/lowcode-editor-core'; -import { Project } from '../../../src/project/project'; +import { Editor, Setters } from '@alilc/lowcode-editor-core'; import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; -import formSchema from '../../fixtures/schema/form'; import settingSchema from '../../fixtures/schema/setting'; import divMeta from '../../fixtures/component-metadata/div'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; const editor = new Editor(); describe('setting-top-entry 测试', () => { let designer: Designer; beforeEach(() => { - designer = new Designer({ editor }); + editor.set('setters', new Setters()) + designer = new Designer({ editor, shellModelFactory }); }); afterEach(() => { designer._componentMetasMap.clear(); diff --git a/packages/designer/tests/document/document-model/__snapshots__/document-model.test.ts.snap b/packages/designer/tests/document/document-model/__snapshots__/document-model.test.ts.snap index 6448ab03dc..9063133031 100644 --- a/packages/designer/tests/document/document-model/__snapshots__/document-model.test.ts.snap +++ b/packages/designer/tests/document/document-model/__snapshots__/document-model.test.ts.snap @@ -98,10 +98,10 @@ Object { "__style__": Object {}, "behavior": "NORMAL", "content": Object { - "en_US": "Title", + "en-US": "Title", "type": "i18n", - "use": "zh_CN", - "zh_CN": "个人信息", + "use": "zh-CN", + "zh-CN": "个人信息", }, "fieldId": "text_k1ow3h1j", "maxLine": 0, @@ -148,13 +148,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -166,10 +166,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "姓名", + "use": "zh-CN", + "zh-CN": "姓名", }, "labelAlign": "top", "labelColOffset": 0, @@ -177,25 +177,25 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [ @@ -205,8 +205,8 @@ Object { ], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -226,13 +226,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -244,10 +244,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "英文名", + "use": "zh-CN", + "zh-CN": "英文名", }, "labelAlign": "top", "labelColOffset": 0, @@ -255,32 +255,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -300,13 +300,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -318,10 +318,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "职位", + "use": "zh-CN", + "zh-CN": "职位", }, "labelAlign": "top", "labelColOffset": 0, @@ -329,32 +329,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -390,13 +390,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -408,10 +408,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "花名", + "use": "zh-CN", + "zh-CN": "花名", }, "labelAlign": "top", "labelColOffset": 0, @@ -419,32 +419,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -471,9 +471,9 @@ Object { "sid": "opt_k1owc4t2", "text": Object { "__sid__": "param_k1owc4tb", - "en_US": "Option 1", + "en-US": "Option 1", "type": "i18n", - "zh_CN": "男", + "zh-CN": "男", }, "value": "M", }, @@ -483,9 +483,9 @@ Object { "sid": "opt_k1owc4t3", "text": Object { "__sid__": "param_k1owc4tf", - "en_US": "Option 2", + "en-US": "Option 2", "type": "i18n", - "zh_CN": "女", + "zh-CN": "女", }, "value": "F", }, @@ -498,10 +498,10 @@ Object { "hasClear": false, "hasSelectAll": false, "label": Object { - "en_US": "SelectField", + "en-US": "SelectField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "性别", + "use": "zh-CN", + "zh-CN": "性别", }, "labelAlign": "top", "labelColOffset": 0, @@ -509,30 +509,30 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "mode": "single", "notFoundContent": Object { "type": "i18n", - "use": "zh_CN", + "use": "zh-CN", }, "placeholder": Object { - "en_US": "please select", + "en-US": "please select", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请选择", + "use": "zh-CN", + "zh-CN": "请选择", }, "searchDelay": 300, "showSearch": false, "size": "medium", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "validation": Array [ Object { @@ -604,23 +604,23 @@ Object { "dividerNoInset": false, "extra": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "fieldId": "card_k1ow3h1l", "showHeadDivider": true, "showTitleBullet": true, "subTitle": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "title": Object { - "en_US": "Title", + "en-US": "Title", "type": "i18n", - "use": "zh_CN", - "zh_CN": "基本信息", + "use": "zh-CN", + "zh-CN": "基本信息", }, }, "title": "", @@ -642,13 +642,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -660,10 +660,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "所属部门", + "use": "zh-CN", + "zh-CN": "所属部门", }, "labelAlign": "top", "labelColOffset": 0, @@ -671,32 +671,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -720,13 +720,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -738,10 +738,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "主管", + "use": "zh-CN", + "zh-CN": "主管", }, "labelAlign": "top", "labelColOffset": 0, @@ -749,32 +749,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -810,13 +810,13 @@ Object { "__useMediator": "value", "addonAfter": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "addonBefore": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "autoFocus": false, "autoHeight": false, @@ -828,10 +828,10 @@ Object { "hasLimitHint": false, "htmlType": "input", "label": Object { - "en_US": "TextField", + "en-US": "TextField", "type": "i18n", - "use": "zh_CN", - "zh_CN": "HRG", + "use": "zh-CN", + "zh-CN": "HRG", }, "labelAlign": "top", "labelColOffset": 0, @@ -839,32 +839,32 @@ Object { "labelTextAlign": "right", "labelTipsIcon": "", "labelTipsText": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "labelTipsTypes": "none", "placeholder": Object { - "en_US": "please input", + "en-US": "please input", "type": "i18n", - "use": "zh_CN", - "zh_CN": "请输入", + "use": "zh-CN", + "zh-CN": "请输入", }, "rows": 4, "size": "medium", "state": "", "tips": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "zh_CN": "", + "zh-CN": "", }, "trim": false, "validation": Array [], "value": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "wrapperColOffset": 0, "wrapperColSpan": 0, @@ -930,23 +930,23 @@ Object { "dividerNoInset": false, "extra": Object { "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "fieldId": "card_k1ow3h1m", "showHeadDivider": true, "showTitleBullet": true, "subTitle": Object { - "en_US": "", + "en-US": "", "type": "i18n", - "use": "zh_CN", - "zh_CN": "", + "use": "zh-CN", + "zh-CN": "", }, "title": Object { - "en_US": "Title", + "en-US": "Title", "type": "i18n", - "use": "zh_CN", - "zh_CN": "部门信息", + "use": "zh-CN", + "zh-CN": "部门信息", }, }, "title": "", @@ -969,10 +969,10 @@ Object { "behavior": "NORMAL", "className": "button_kgaqfbm7", "content": Object { - "en_US": "Button", + "en-US": "Button", "type": "i18n", - "use": "zh_CN", - "zh_CN": "提交", + "use": "zh-CN", + "zh-CN": "提交", }, "fieldId": "button_k1ow3h1n", "loading": false, @@ -1012,10 +1012,10 @@ Object { "behavior": "NORMAL", "className": "button_kgaqfbm8", "content": Object { - "en_US": "Button", + "en-US": "Button", "type": "i18n", - "use": "zh_CN", - "zh_CN": "取消", + "use": "zh-CN", + "zh-CN": "取消", }, "fieldId": "button_k1ow3h1p", "greeting": Object { diff --git a/packages/designer/tests/document/document-model/document-model.test.ts b/packages/designer/tests/document/document-model/document-model.test.ts index c110116a63..b47200cbaf 100644 --- a/packages/designer/tests/document/document-model/document-model.test.ts +++ b/packages/designer/tests/document/document-model/document-model.test.ts @@ -2,15 +2,13 @@ import '../../fixtures/window'; import { DocumentModel, isDocumentModel, isPageSchema } from '../../../src/document/document-model'; import { Editor } from '@alilc/lowcode-editor-core'; import { Project } from '../../../src/project/project'; -import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; import divMeta from '../../fixtures/component-metadata/div'; import formMeta from '../../fixtures/component-metadata/form'; import otherMeta from '../../fixtures/component-metadata/other'; import pageMeta from '../../fixtures/component-metadata/page'; -// const { DocumentModel } = require('../../../src/document/document-model'); -// const { Node } = require('../__mocks__/node'); +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; describe('document-model 测试', () => { let editor: Editor; @@ -19,13 +17,13 @@ describe('document-model 测试', () => { beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; }); it('empty schema', () => { const doc = new DocumentModel(project); - expect(doc.rootNode.id).toBe('root'); + expect(doc.rootNode?.id).toBe('root'); expect(doc.currentRoot).toBe(doc.rootNode); expect(doc.root).toBe(doc.rootNode); expect(doc.modalNode).toBeUndefined(); @@ -62,7 +60,7 @@ describe('document-model 测试', () => { doc.internalRemoveAndPurgeNode({ id: 'mockId' }); // internalSetDropLocation - doc.internalSetDropLocation({ a: 1 }); + doc.dropLocation = { a: 1 }; expect(doc.dropLocation).toEqual({ a: 1 }); // wrapWith @@ -219,6 +217,7 @@ describe('document-model 测试', () => { it('checkNesting / checkDropTarget / checkNestingUp / checkNestingDown', () => { designer.createComponentMeta(pageMeta); designer.createComponentMeta(formMeta); + designer.createComponentMeta(otherMeta); const doc = new DocumentModel(project, formSchema); expect( @@ -240,6 +239,26 @@ describe('document-model 测试', () => { data: { componentName: 'Form' }, }), ).toBeTruthy(); + expect( + doc.checkNesting(doc.getNode('page'), doc.getNode('form')) + ).toBeTruthy(); + expect( + doc.checkNesting(doc.getNode('page'), null) + ).toBeTruthy(); + expect( + doc.checkNesting(doc.getNode('page'), { + type: 'nodedata', + data: { componentName: 'Other' }, + }) + ).toBeFalsy(); + + expect( + doc.checkNestingUp(doc.getNode('page'), { componentName: 'Other' }) + ).toBeFalsy(); + + expect( + doc.checkNestingDown(doc.getNode('page'), { componentName: 'Other' }) + ).toBeTruthy(); expect(doc.checkNestingUp(doc.getNode('page'), null)).toBeTruthy(); }); diff --git a/packages/designer/tests/document/history/history.test.ts b/packages/designer/tests/document/history/history.test.ts index a4c6d3a66b..63af8ecbf3 100644 --- a/packages/designer/tests/document/history/history.test.ts +++ b/packages/designer/tests/document/history/history.test.ts @@ -2,6 +2,7 @@ import '../../fixtures/window'; import { mobx, makeAutoObservable, globalContext, Editor } from '@alilc/lowcode-editor-core'; import { History } from '../../../src/document/history'; import { delay } from '../../utils/misc'; +import { Workspace } from '@alilc/lowcode-workspace'; class Node { data: number; @@ -36,7 +37,10 @@ afterEach(() => { describe('History', () => { beforeAll(() => { - globalContext.register(new Editor(), Editor); + const editor = new Editor(); + globalContext.register(editor, Editor); + globalContext.register(editor, 'editor'); + globalContext.register(new Workspace(), 'workspace'); }); it('data function & records', async () => { @@ -272,20 +276,6 @@ describe('History', () => { expect(history.records).toHaveLength(0); }); - it('internalToShellHistory()', async () => { - const history = new History<Node>( - () => { - const data = tree.toObject(); - return data; - }, - (data) => { - mockRedoFn(data); - }, - ); - - expect(history.internalToShellHistory().isModified).toBeUndefined(); - }); - it('sleep & wakeup', async () => { const mockRedoFn = jest.fn(); const history = new History<Node>( diff --git a/packages/designer/tests/document/node/modal-nodes-manager.test.ts b/packages/designer/tests/document/node/modal-nodes-manager.test.ts index 80ca53b3e5..3e5dcdb79e 100644 --- a/packages/designer/tests/document/node/modal-nodes-manager.test.ts +++ b/packages/designer/tests/document/node/modal-nodes-manager.test.ts @@ -7,6 +7,7 @@ import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form-with-modal'; import dlgMetadata from '../../fixtures/component-metadata/dialog'; import { getModalNodes } from '../../../src/document/node/modal-nodes-manager'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; let editor: Editor; let designer: Designer; @@ -15,7 +16,7 @@ let doc: DocumentModel; beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); designer.createComponentMeta(dlgMetadata); project = designer.project; doc = new DocumentModel(project, formSchema); diff --git a/packages/designer/tests/document/node/node-children.test.ts b/packages/designer/tests/document/node/node-children.test.ts index c48c9e7085..1aa7e3ccb3 100644 --- a/packages/designer/tests/document/node/node-children.test.ts +++ b/packages/designer/tests/document/node/node-children.test.ts @@ -8,6 +8,7 @@ import { import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; import divMetadata from '../../fixtures/component-metadata/div'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; describe('NodeChildren 方法测试', () => { let editor: Editor; @@ -17,7 +18,7 @@ describe('NodeChildren 方法测试', () => { beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = new DocumentModel(project, formSchema); }); diff --git a/packages/designer/tests/document/node/node.add.test.ts b/packages/designer/tests/document/node/node.add.test.ts index a9ca5247ce..87a4222cd0 100644 --- a/packages/designer/tests/document/node/node.add.test.ts +++ b/packages/designer/tests/document/node/node.add.test.ts @@ -1,12 +1,11 @@ import set from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; -import { Project } from '../../../src/project/project'; -import { Node } from '../../../src/document/node/node'; +import { Project, IProject } from '../../../src/project/project'; +import { Node, INode } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; -import { EBADF } from 'constants'; const mockCreateSettingEntry = jest.fn(); jest.mock('../../../src/designer/designer', () => { @@ -18,6 +17,9 @@ jest.mock('../../../src/designer/designer', () => { getMetadata() { return { configure: { advanced: null } }; }, + get advanced() { + return {}; + }, }; }, transformProps(props) { return props; }, @@ -35,7 +37,7 @@ beforeAll(() => { describe('schema 生成节点模型测试', () => { describe('block ❌ | component ❌ | slot ❌', () => { - let project: Project; + let project: IProject; beforeEach(() => { project = new Project(designer, { componentsTree: [ @@ -50,12 +52,12 @@ describe('schema 生成节点模型测试', () => { it('基本的节点模型初始化,模型导出', () => { expect(project).toBeTruthy(); const { currentDocument } = project; - const { nodesMap } = currentDocument; + const nodesMap = currentDocument?.nodesMap; const ids = getIdsFromSchema(formSchema); const expectedNodeCnt = ids.length; - expect(nodesMap.size).toBe(expectedNodeCnt); + expect(nodesMap?.size).toBe(expectedNodeCnt); ids.forEach(id => { - expect(nodesMap.get(id).componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); + expect(nodesMap?.get(id)?.componentName).toBe(getNodeFromSchemaById(formSchema, id).componentName); }); const pageNode = currentDocument?.getNode('page'); @@ -74,18 +76,18 @@ describe('schema 生成节点模型测试', () => { it('基本的节点模型初始化,节点深度', () => { expect(project).toBeTruthy(); const { currentDocument } = project; - const getNode = currentDocument.getNode.bind(currentDocument); - - const pageNode = getNode('page'); - const rootHeaderNode = getNode('node_k1ow3cba'); - const rootContentNode = getNode('node_k1ow3cbb'); - const rootFooterNode = getNode('node_k1ow3cbc'); - const formNode = getNode('form'); - const cardNode = getNode('node_k1ow3cbj'); - const cardContentNode = getNode('node_k1ow3cbk'); - const columnsLayoutNode = getNode('node_k1ow3cbw'); - const columnNode = getNode('node_k1ow3cbx'); - const textFieldNode = getNode('node_k1ow3cbz'); + const getNode = currentDocument?.getNode.bind(currentDocument); + + const pageNode = getNode?.('page'); + const rootHeaderNode = getNode?.('node_k1ow3cba'); + const rootContentNode = getNode?.('node_k1ow3cbb'); + const rootFooterNode = getNode?.('node_k1ow3cbc'); + const formNode = getNode?.('form'); + const cardNode = getNode?.('node_k1ow3cbj'); + const cardContentNode = getNode?.('node_k1ow3cbk'); + const columnsLayoutNode = getNode?.('node_k1ow3cbw'); + const columnNode = getNode?.('node_k1ow3cbx'); + const textFieldNode = getNode?.('node_k1ow3cbz'); expect(pageNode?.zLevel).toBe(0); expect(rootHeaderNode?.zLevel).toBe(1); @@ -129,7 +131,7 @@ describe('schema 生成节点模型测试', () => { const textFieldNode = getNode('node_k1ow3cbz'); expect(pageNode?.index).toBe(-1); - expect(pageNode?.children.toString()).toBe('[object Array]'); + expect(pageNode?.children?.toString()).toBe('[object Array]'); expect(pageNode?.children?.get(1)).toBe(rootContentNode); expect(pageNode?.getChildren()?.get(1)).toBe(rootContentNode); expect(pageNode?.getNode()).toBe(pageNode); @@ -160,20 +162,20 @@ describe('schema 生成节点模型测试', () => { it('基本的节点模型初始化,节点新建、删除等事件', () => { expect(project).toBeTruthy(); const { currentDocument } = project; - const getNode = currentDocument.getNode.bind(currentDocument); - const createNode = currentDocument.createNode.bind(currentDocument); + const getNode = currentDocument?.getNode.bind(currentDocument); + const createNode = currentDocument?.createNode.bind(currentDocument); - const pageNode = getNode('page'); + const pageNode = getNode?.('page'); const nodeCreateHandler = jest.fn(); const offCreate = currentDocument?.onNodeCreate(nodeCreateHandler); - const node = createNode({ + const node = createNode?.({ componentName: 'TextInput', props: { propA: 'haha', }, }); - currentDocument?.insertNode(pageNode, node); + pageNode && node && currentDocument?.insertNode(pageNode, node); expect(nodeCreateHandler).toHaveBeenCalledTimes(1); expect(nodeCreateHandler.mock.calls[0][0]).toBe(node); @@ -182,7 +184,7 @@ describe('schema 生成节点模型测试', () => { const nodeDestroyHandler = jest.fn(); const offDestroy = currentDocument?.onNodeDestroy(nodeDestroyHandler); - node.remove(); + node?.remove(); expect(nodeDestroyHandler).toHaveBeenCalledTimes(1); expect(nodeDestroyHandler.mock.calls[0][0]).toBe(node); expect(nodeDestroyHandler.mock.calls[0][0].componentName).toBe('TextInput'); @@ -288,9 +290,9 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form'); - currentDocument?.insertNode(formNode, { + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form'); + formNode && currentDocument?.insertNode(formNode, { componentName: 'TextInput', id: 'nodeschema-id1', props: { @@ -298,11 +300,11 @@ describe('schema 生成节点模型测试', () => { propB: 3, }, }, 0); - expect(nodesMap.size).toBe(ids.length + 1); - expect(formNode.children.length).toBe(4); - const insertedNode = formNode.children.get(0); - expect(insertedNode.componentName).toBe('TextInput'); - expect(insertedNode.propsData).toEqual({ + expect(nodesMap?.size).toBe(ids.length + 1); + expect(formNode?.children?.length).toBe(4); + const insertedNode = formNode?.children?.get(0); + expect(insertedNode?.componentName).toBe('TextInput'); + expect(insertedNode?.propsData).toEqual({ propA: 'haha', propB: 3, }); @@ -314,9 +316,9 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form'); - currentDocument?.insertNode(formNode, { + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form'); + formNode && currentDocument?.insertNode(formNode, { componentName: 'TextInput', id: 'nodeschema-id1', props: { @@ -324,11 +326,11 @@ describe('schema 生成节点模型测试', () => { propB: 3, }, }, 1); - expect(nodesMap.size).toBe(ids.length + 1); - expect(formNode.children.length).toBe(4); - const insertedNode = formNode.children.get(1); - expect(insertedNode.componentName).toBe('TextInput'); - expect(insertedNode.propsData).toEqual({ + expect(nodesMap?.size).toBe(ids.length + 1); + expect(formNode?.children?.length).toBe(4); + const insertedNode = formNode?.children?.get(1); + expect(insertedNode?.componentName).toBe('TextInput'); + expect(insertedNode?.propsData).toEqual({ propA: 'haha', propB: 3, }); @@ -340,8 +342,8 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form') as Node; + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form') as INode; currentDocument?.insertNode(formNode, { componentName: 'ParentNode', props: { @@ -365,8 +367,8 @@ describe('schema 生成节点模型测试', () => { }, ], }); - expect(nodesMap.size).toBe(ids.length + 3); - expect(formNode.children.length).toBe(4); + expect(nodesMap?.size).toBe(ids.length + 3); + expect(formNode.children?.length).toBe(4); expect(formNode.children?.get(3)?.componentName).toBe('ParentNode'); expect(formNode.children?.get(3)?.children?.get(0)?.componentName).toBe('SubNode'); expect(formNode.children?.get(3)?.children?.get(1)?.componentName).toBe('SubNode2'); @@ -376,9 +378,9 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form'); - currentDocument?.insertNode(formNode, { + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form'); + formNode && currentDocument?.insertNode(formNode, { componentName: 'TextInput', id: 'nodeschema-id1', props: { @@ -386,17 +388,17 @@ describe('schema 生成节点模型测试', () => { propB: 3, }, }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); + expect(nodesMap?.get('nodeschema-id1')?.componentName).toBe('TextInput'); + expect(nodesMap?.size).toBe(ids.length + 1); }); it.skip('场景一:插入 NodeSchema,id 与现有 schema 里的 id 重复,但关闭了 id 检测器', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form'); - currentDocument?.insertNode(formNode, { + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form'); + formNode && currentDocument?.insertNode(formNode, { componentName: 'TextInput', id: 'nodeschema-id1', props: { @@ -404,16 +406,16 @@ describe('schema 生成节点模型测试', () => { propB: 3, }, }); - expect(nodesMap.get('nodeschema-id1').componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); + expect(nodesMap?.get('nodeschema-id1')?.componentName).toBe('TextInput'); + expect(nodesMap?.size).toBe(ids.length + 1); }); it('场景二:插入 Node 实例', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form'); + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form'); const inputNode = currentDocument?.createNode({ componentName: 'TextInput', id: 'nodeschema-id2', @@ -422,22 +424,22 @@ describe('schema 生成节点模型测试', () => { propB: 3, }, }); - currentDocument?.insertNode(formNode, inputNode); - expect(formNode.children?.get(3)?.componentName).toBe('TextInput'); - expect(nodesMap.size).toBe(ids.length + 1); + formNode && currentDocument?.insertNode(formNode, inputNode); + expect(formNode?.children?.get(3)?.componentName).toBe('TextInput'); + expect(nodesMap?.size).toBe(ids.length + 1); }); it('场景三:插入 JSExpression', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form') as Node; + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form') as Node; currentDocument?.insertNode(formNode, { type: 'JSExpression', value: 'just a expression', }); - expect(nodesMap.size).toBe(ids.length + 1); + expect(nodesMap?.size).toBe(ids.length + 1); expect(formNode.children?.get(3)?.componentName).toBe('Leaf'); // expect(formNode.children?.get(3)?.children).toEqual({ // type: 'JSExpression', @@ -448,10 +450,10 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form') as Node; + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form') as Node; currentDocument?.insertNode(formNode, 'just a string'); - expect(nodesMap.size).toBe(ids.length + 1); + expect(nodesMap?.size).toBe(ids.length + 1); expect(formNode.children?.get(3)?.componentName).toBe('Leaf'); // expect(formNode.children?.get(3)?.children).toBe('just a string'); }); @@ -471,8 +473,8 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form') as Node; + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form') as Node; const formNode2 = currentDocument?.getNode('form'); expect(formNode).toEqual(formNode2); currentDocument?.insertNodes(formNode, [ @@ -491,28 +493,28 @@ describe('schema 生成节点模型测试', () => { }, }, ], 1); - expect(nodesMap.size).toBe(ids.length + 2); + expect(nodesMap?.size).toBe(ids.length + 2); expect(formNode.children?.length).toBe(5); - const insertedNode1 = formNode.children.get(1); - const insertedNode2 = formNode.children.get(2); - expect(insertedNode1.componentName).toBe('TextInput'); - expect(insertedNode1.propsData).toEqual({ + const insertedNode1 = formNode.children?.get(1); + const insertedNode2 = formNode.children?.get(2); + expect(insertedNode1?.componentName).toBe('TextInput'); + expect(insertedNode1?.propsData).toEqual({ propA: 'haha2', propB: 3, }); - expect(insertedNode2.componentName).toBe('TextInput2'); - expect(insertedNode2.propsData).toEqual({ + expect(insertedNode2?.componentName).toBe('TextInput2'); + expect(insertedNode2?.propsData).toEqual({ propA: 'haha', propB: 3, }); }); - it('场景二:插入 Node 实例,指定 index', () => { + it.only('场景二:插入 Node 实例,指定 index', () => { expect(project).toBeTruthy(); const ids = getIdsFromSchema(formSchema); const { currentDocument } = project; - const { nodesMap } = currentDocument; - const formNode = nodesMap.get('form') as Node; + const nodesMap = currentDocument?.nodesMap; + const formNode = nodesMap?.get('form') as INode; const formNode2 = currentDocument?.getNode('form'); expect(formNode).toEqual(formNode2); const createdNode1 = currentDocument?.createNode({ @@ -530,17 +532,17 @@ describe('schema 生成节点模型测试', () => { }, }); currentDocument?.insertNodes(formNode, [createdNode1, createdNode2], 1); - expect(nodesMap.size).toBe(ids.length + 2); + expect(nodesMap?.size).toBe(ids.length + 2); expect(formNode.children?.length).toBe(5); - const insertedNode1 = formNode.children.get(1); - const insertedNode2 = formNode.children.get(2); - expect(insertedNode1.componentName).toBe('TextInput'); - expect(insertedNode1.propsData).toEqual({ + const insertedNode1 = formNode.children?.get(1); + const insertedNode2 = formNode.children?.get(2); + expect(insertedNode1?.componentName).toBe('TextInput'); + expect(insertedNode1?.propsData).toEqual({ propA: 'haha2', propB: 3, }); - expect(insertedNode2.componentName).toBe('TextInput2'); - expect(insertedNode2.propsData).toEqual({ + expect(insertedNode2?.componentName).toBe('TextInput2'); + expect(insertedNode2?.propsData).toEqual({ propA: 'haha', propB: 3, }); @@ -559,13 +561,13 @@ describe('schema 生成节点模型测试', () => { project.open(); expect(project).toBeTruthy(); const { currentDocument } = project; - const { nodesMap } = currentDocument; + const nodesMap = currentDocument?.nodesMap; const ids = getIdsFromSchema(formSchema); // 目前每个 slot 会新增(1 + children.length)个节点 const expectedNodeCnt = ids.length + 2; - expect(nodesMap.size).toBe(expectedNodeCnt); + expect(nodesMap?.size).toBe(expectedNodeCnt); // PageHeader - expect(nodesMap.get('node_k1ow3cbd').slots).toHaveLength(1); + expect(nodesMap?.get('node_k1ow3cbd')?.slots).toHaveLength(1); }); }); }); diff --git a/packages/designer/tests/document/node/node.dragdrop.test.ts b/packages/designer/tests/document/node/node.dragdrop.test.ts index c15ced2b25..3fe9091245 100644 --- a/packages/designer/tests/document/node/node.dragdrop.test.ts +++ b/packages/designer/tests/document/node/node.dragdrop.test.ts @@ -1,8 +1,5 @@ -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; import { Project } from '../../../src/project/project'; -import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; diff --git a/packages/designer/tests/document/node/node.modify.test.ts b/packages/designer/tests/document/node/node.modify.test.ts index 34db3b238f..f7bd7dd5ed 100644 --- a/packages/designer/tests/document/node/node.modify.test.ts +++ b/packages/designer/tests/document/node/node.modify.test.ts @@ -1,8 +1,5 @@ -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; import { Project } from '../../../src/project/project'; -import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; @@ -17,6 +14,9 @@ jest.mock('../../../src/designer/designer', () => { getMetadata() { return { configure: { advanced: null } }; }, + get advanced() { + return {}; + }, }; }, transformProps(props) { return props; }, diff --git a/packages/designer/tests/document/node/node.remove.test.ts b/packages/designer/tests/document/node/node.remove.test.ts index 1adf7dd0b3..82d1804434 100644 --- a/packages/designer/tests/document/node/node.remove.test.ts +++ b/packages/designer/tests/document/node/node.remove.test.ts @@ -2,10 +2,9 @@ import set from 'lodash/set'; import cloneDeep from 'lodash/cloneDeep'; import '../../fixtures/window'; import { Project } from '../../../src/project/project'; -import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import formSchema from '../../fixtures/schema/form'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../../utils'; +import { getIdsFromSchema } from '../../utils'; const mockCreateSettingEntry = jest.fn(); jest.mock('../../../src/designer/designer', () => { @@ -17,6 +16,9 @@ jest.mock('../../../src/designer/designer', () => { getMetadata() { return { configure: { advanced: null } }; }, + get advanced() { + return {}; + }, }; }, transformProps(props) { return props; }, diff --git a/packages/designer/tests/document/node/node.test.ts b/packages/designer/tests/document/node/node.test.ts index 113360d448..2695d6c838 100644 --- a/packages/designer/tests/document/node/node.test.ts +++ b/packages/designer/tests/document/node/node.test.ts @@ -1,17 +1,19 @@ // @ts-nocheck import '../../fixtures/window'; -import { set, delayObxTick, delay } from '../../utils'; -import { Editor } from '@alilc/lowcode-editor-core'; +import { set } from '../../utils'; +import { + Editor, + globalContext, + Setters as InnerSetters, +} from '@alilc/lowcode-editor-core'; import { Project } from '../../../src/project/project'; +import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace'; import { DocumentModel } from '../../../src/document/document-model'; import { isRootNode, Node, - isNode, comparePosition, contains, - insertChild, - insertChildren, PositionNO, } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; @@ -20,11 +22,13 @@ import divMetadata from '../../fixtures/component-metadata/div'; import dialogMetadata from '../../fixtures/component-metadata/dialog'; import btnMetadata from '../../fixtures/component-metadata/button'; import formMetadata from '../../fixtures/component-metadata/form'; -import otherMeta from '../../fixtures/component-metadata/other'; import pageMetadata from '../../fixtures/component-metadata/page'; import rootHeaderMetadata from '../../fixtures/component-metadata/root-header'; import rootContentMetadata from '../../fixtures/component-metadata/root-content'; import rootFooterMetadata from '../../fixtures/component-metadata/root-footer'; +import { shellModelFactory } from '../../../../engine/src/modules/shell-model-factory'; +import { isNode } from '@alilc/lowcode-utils'; +import { Setters } from '@alilc/lowcode-shell'; describe('Node 方法测试', () => { let editor: Editor; @@ -34,9 +38,12 @@ describe('Node 方法测试', () => { beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = new DocumentModel(project, formSchema); + editor.set('setters', new Setters(new InnerSetters())); + !globalContext.has(Editor) && globalContext.register(editor, Editor); + !globalContext.has('workspace') && globalContext.register(new InnerWorkspace(), 'workspace'); }); afterEach(() => { @@ -47,7 +54,60 @@ describe('Node 方法测试', () => { project = null; }); - it('condition group', () => {}); + // Case 1: When children is null + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const result = node.initialChildren(null); + // 预期结果是一个空数组 + expect(result).toEqual([]); + }); + + // Case 2: When children is undefined + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const result = node.initialChildren(undefined); + // 预期结果是一个空数组 + expect(result).toEqual([]); + }); + + // Case 3: When children is array + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const childrenArray = [{ id: 1, name: 'Child 1' }, { id: 2, name: 'Child 2' }]; + const result = node.initialChildren(childrenArray); + // 预期结果是一个数组 + expect(result).toEqual(childrenArray); + }); + + // Case 4: When children is not null and not an array + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const childObject = { id: 1, name: 'Child 1' }; + const result = node.initialChildren(childObject); + // 预期结果是一个数组 + expect(result).toEqual([childObject]); + }); + + // Case 5: When children 0 + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const childObject = 0; + const result = node.initialChildren(childObject); + // 预期结果是一个数组 + expect(result).toEqual([0]); + }); + + // Case 6: When children false + test('initialChildren returns result of initialChildren function when children is null ', () => { + const node = new Node(doc, { componentName: 'Button', props: { a: 1 } }); + const childObject = false; + const result = node.initialChildren(childObject); + // 预期结果是一个数组 + expect(result).toEqual([false]); + }); + + + it('condition group', () => { }); it('getExtraProp / setExtraProp', () => { const firstBtn = doc.getNode('node_k1ow3cbn')!; @@ -360,7 +420,7 @@ describe('Node 方法测试', () => { expect(mockFn).not.toHaveBeenCalled(); }); - it('addSlot / unlinkSlot / removeSlot', () => {}); + it('addSlot / unlinkSlot / removeSlot', () => { }); it('setProps', () => { const firstBtn = doc.getNode('node_k1ow3cbn')!; @@ -400,7 +460,7 @@ describe('Node 方法测试', () => { designer.createComponentMeta(btnMetadata); const btn = doc.getNode('node_k1ow3cbn'); // 从 componentMeta 中获取到 title 值 - expect(btn.title).toEqual({ type: 'i18n', 'zh-CN': '按钮', 'en-US': 'Button' } ); + expect(btn.title).toEqual({ type: 'i18n', 'zh-CN': '按钮', 'en-US': 'Button' }); // 从 extraProp 中获取值 btn.setExtraProp('title', 'hello button'); expect(btn.title).toBe('hello button'); @@ -475,7 +535,7 @@ describe('Node 方法测试', () => { const form = doc.getNode('node_k1ow3cbo'); designer.createComponentMeta(divMetadata); designer.createComponentMeta(formMetadata); - const callbacks = form.componentMeta.getMetadata().configure.advanced?.callbacks; + const callbacks = form.componentMeta.advanced.callbacks; const fn1 = callbacks.onNodeAdd = jest.fn(); const fn2 = callbacks.onNodeRemove = jest.fn(); const textField = doc.getNode('node_k1ow3cc9'); @@ -539,7 +599,7 @@ describe('Node 方法测试', () => { expect(comparePosition(firstBtn, firstCard)).toBe(PositionNO.BeforeOrAfter); }); - it('getZLevelTop', () => {}); + it('getZLevelTop', () => { }); it('propsData', () => { expect(new Node(doc, { componentName: 'Leaf' }).propsData).toBeNull(); expect(new Node(doc, { componentName: 'Fragment' }).propsData).toBeNull(); diff --git a/packages/designer/tests/document/node/props/prop.test.ts b/packages/designer/tests/document/node/props/prop.test.ts index d7b125430d..ff4147a34a 100644 --- a/packages/designer/tests/document/node/props/prop.test.ts +++ b/packages/designer/tests/document/node/props/prop.test.ts @@ -1,11 +1,10 @@ -// @ts-nocheck import '../../../fixtures/window'; -import { delayObxTick } from '../../../utils'; 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 { TransformStage } from '@alilc/lowcode-types'; +import { GlobalEvent, IPublicEnumTransformStage } from '@alilc/lowcode-types'; +import { shellModelFactory } from '../../../../../engine/src/modules/shell-model-factory'; const slotNodeImportMockFn = jest.fn(); const slotNodeRemoveMockFn = jest.fn(); @@ -25,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 类测试', () => { @@ -134,12 +143,12 @@ describe('Prop 类测试', () => { }); it('export', () => { - expect(boolProp.export(TransformStage.Save)).toBe(true); - expect(strProp.export(TransformStage.Save)).toBe('haha'); - expect(numProp.export(TransformStage.Save)).toBe(1); - expect(nullProp.export(TransformStage.Save)).toBe(''); - expect(nullProp.export(TransformStage.Serilize)).toBe(null); - expect(expProp.export(TransformStage.Save)).toEqual({ + expect(boolProp.export(IPublicEnumTransformStage.Save)).toBe(true); + expect(strProp.export(IPublicEnumTransformStage.Save)).toBe('haha'); + expect(numProp.export(IPublicEnumTransformStage.Save)).toBe(1); + expect(nullProp.export(IPublicEnumTransformStage.Save)).toBe(null); + expect(nullProp.export(IPublicEnumTransformStage.Serilize)).toBe(null); + expect(expProp.export(IPublicEnumTransformStage.Save)).toEqual({ type: 'JSExpression', value: 'state.haha', }); @@ -147,16 +156,16 @@ describe('Prop 类测试', () => { strProp.unset(); expect(strProp.getValue()).toBeUndefined(); expect(strProp.isUnset()).toBeTruthy(); - expect(strProp.export(TransformStage.Save)).toBeUndefined(); + expect(strProp.export(IPublicEnumTransformStage.Save)).toBeUndefined(); expect( - new Prop(mockPropsInst, false, '___condition___').export(TransformStage.Render), + new Prop(mockPropsInst, false, '___condition___').export(IPublicEnumTransformStage.Render), ).toBeTruthy(); engineConfig.set('enableCondition', true); expect( - new Prop(mockPropsInst, false, '___condition___').export(TransformStage.Render), + new Prop(mockPropsInst, false, '___condition___').export(IPublicEnumTransformStage.Render), ).toBeFalsy(); - expect(slotProp.export(TransformStage.Render)).toEqual({ + expect(slotProp.export(IPublicEnumTransformStage.Render)).toEqual({ type: 'JSSlot', params: { a: 1 }, value: { @@ -167,7 +176,7 @@ describe('Prop 类测试', () => { children: [{ componentName: 'Button' }], }, }); - expect(slotProp.export(TransformStage.Save)).toEqual({ + expect(slotProp.export(IPublicEnumTransformStage.Save)).toEqual({ type: 'JSSlot', params: { a: 1 }, value: [{ componentName: 'Button' }], @@ -380,7 +389,6 @@ describe('Prop 类测试', () => { prop.dispose(); expect(prop._items).toBeNull(); - expect(prop._maps).toBeNull(); }); }); @@ -436,7 +444,7 @@ describe('Prop 类测试', () => { it('should return undefined when all items are undefined', () => { prop = new Prop(mockPropsInst, [undefined, undefined], '___loopArgs___'); - expect(prop.getValue()).toBeUndefined(); + expect(prop.getValue()).toEqual([undefined, undefined]); }); it('迭代器 / map / forEach', () => { @@ -465,7 +473,7 @@ describe('Prop 类测试', () => { describe('slotNode / setAsSlot', () => { const editor = new Editor(); - const designer = new Designer({ editor }); + const designer = new Designer({ editor, shellModelFactory }); const doc = new DocumentModel(designer.project, { componentName: 'Page', children: [ @@ -494,7 +502,60 @@ describe('Prop 类测试', () => { slotProp.export(); expect(slotProp.export().value[0].componentName).toBe('Button'); - expect(slotProp.export(TransformStage.Serilize).value[0].componentName).toBe('Button'); + expect(slotProp.export(IPublicEnumTransformStage.Serilize).value[0].componentName).toBe('Button'); + + slotProp.purge(); + expect(slotProp.purged).toBeTruthy(); + slotProp.dispose(); + }); + + describe('slotNode-value / setAsSlot', () => { + const editor = new Editor(); + const designer = new Designer({ editor, shellModelFactory }); + const doc = new DocumentModel(designer.project, { + componentName: 'Page', + children: [ + { + id: 'div', + componentName: 'Div', + }, + ], + }); + const div = doc.getNode('div'); + + const slotProp = new Prop(div?.getProps(), { + type: 'JSSlot', + value: { + componentName: 'Slot', + id: 'node_oclei5rv2e2', + props: { + slotName: "content", + slotTitle: "主内容" + }, + children: [ + { + componentName: 'Button', + } + ] + }, + }); + + expect(slotProp.slotNode?.componentName).toBe('Slot'); + + expect(slotProp.slotNode?.title).toBe('主内容'); + expect(slotProp.slotNode?.getExtraProp('name')?.getValue()).toBe('content'); + expect(slotProp.slotNode?.export()?.id).toBe('node_oclei5rv2e2'); + + slotProp.export(); + + // Save + expect(slotProp.export()?.value[0].componentName).toBe('Button'); + expect(slotProp.export()?.title).toBe('主内容'); + expect(slotProp.export()?.name).toBe('content'); + + // Render + expect(slotProp.export(IPublicEnumTransformStage.Render)?.value.children[0].componentName).toBe('Button'); + expect(slotProp.export(IPublicEnumTransformStage.Render)?.value.componentName).toBe('Slot'); slotProp.purge(); expect(slotProp.purged).toBeTruthy(); @@ -513,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/designer/tests/document/selection.test.ts b/packages/designer/tests/document/selection.test.ts index 30032ac7ab..0af22b5cef 100644 --- a/packages/designer/tests/document/selection.test.ts +++ b/packages/designer/tests/document/selection.test.ts @@ -17,6 +17,9 @@ jest.mock('../../src/designer/designer', () => { getMetadata() { return { configure: { advanced: null } }; }, + get advanced() { + return {}; + }, }; }, transformProps(props) { return props; }, @@ -119,7 +122,7 @@ describe('选择区测试', () => { selectionChangeHandler.mockClear(); }); - it('dispose 方法', () => { + it('selectAll 包含不存在的 id', () => { const project = new Project(designer, { componentsTree: [ formSchema, @@ -132,14 +135,7 @@ describe('选择区测试', () => { selection.selectAll(['form', 'node_k1ow3cbj', 'form2']); - const selectionChangeHandler = jest.fn(); - selection.onSelectionChange(selectionChangeHandler); - selection.dispose(); - - expect(selectionChangeHandler).toHaveBeenCalledTimes(1); - expect(selectionChangeHandler.mock.calls[0][0]).toEqual(['form', 'node_k1ow3cbj']); expect(selection.selected).toEqual(['form', 'node_k1ow3cbj']); - selectionChangeHandler.mockClear(); }); it('dispose 方法 - 选中的节点没有被删除的', () => { diff --git a/packages/designer/tests/fixtures/component-metadata/abcgroup.ts b/packages/designer/tests/fixtures/component-metadata/abcgroup.ts index 986d6e3e93..6b9265ef53 100644 --- a/packages/designer/tests/fixtures/component-metadata/abcgroup.ts +++ b/packages/designer/tests/fixtures/component-metadata/abcgroup.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Abc.Group', npm: { @@ -277,4 +277,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/abcitem.ts b/packages/designer/tests/fixtures/component-metadata/abcitem.ts index c378b35d56..bbccf7119f 100644 --- a/packages/designer/tests/fixtures/component-metadata/abcitem.ts +++ b/packages/designer/tests/fixtures/component-metadata/abcitem.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Abc.Item', npm: { @@ -277,4 +277,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/abcnode.ts b/packages/designer/tests/fixtures/component-metadata/abcnode.ts index f59907194e..b991042a34 100644 --- a/packages/designer/tests/fixtures/component-metadata/abcnode.ts +++ b/packages/designer/tests/fixtures/component-metadata/abcnode.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Abc.Node', npm: { @@ -277,4 +277,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/abcoption.ts b/packages/designer/tests/fixtures/component-metadata/abcoption.ts index 78490060e4..7d9d15a729 100644 --- a/packages/designer/tests/fixtures/component-metadata/abcoption.ts +++ b/packages/designer/tests/fixtures/component-metadata/abcoption.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Abc.Option', npm: { @@ -277,4 +277,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/button.ts b/packages/designer/tests/fixtures/component-metadata/button.ts index ce1bc66365..55186a10c4 100644 --- a/packages/designer/tests/fixtures/component-metadata/button.ts +++ b/packages/designer/tests/fixtures/component-metadata/button.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Button', npm: { @@ -95,7 +95,7 @@ export default { }, ], }, - () => 'haha', // CustomView + () => 'haha', // IPublicTypeCustomView { type: 'group', name: 'groupkgzzeo41', @@ -305,4 +305,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/dialog.ts b/packages/designer/tests/fixtures/component-metadata/dialog.ts index 5e87a471ff..5445d92a6b 100644 --- a/packages/designer/tests/fixtures/component-metadata/dialog.ts +++ b/packages/designer/tests/fixtures/component-metadata/dialog.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Dialog', npm: { @@ -274,4 +274,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div.ts b/packages/designer/tests/fixtures/component-metadata/div.ts index 3c29bf4888..96a5f76ba2 100644 --- a/packages/designer/tests/fixtures/component-metadata/div.ts +++ b/packages/designer/tests/fixtures/component-metadata/div.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -280,4 +280,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div10.ts b/packages/designer/tests/fixtures/component-metadata/div10.ts index 929c34f8c4..9b7c1c4876 100644 --- a/packages/designer/tests/fixtures/component-metadata/div10.ts +++ b/packages/designer/tests/fixtures/component-metadata/div10.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', title: '容器', @@ -19,4 +19,4 @@ export default { }, }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div2.ts b/packages/designer/tests/fixtures/component-metadata/div2.ts index 001266d85c..c9c1be306d 100644 --- a/packages/designer/tests/fixtures/component-metadata/div2.ts +++ b/packages/designer/tests/fixtures/component-metadata/div2.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -277,4 +277,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div3.ts b/packages/designer/tests/fixtures/component-metadata/div3.ts index 6b3717078b..ced3947f41 100644 --- a/packages/designer/tests/fixtures/component-metadata/div3.ts +++ b/packages/designer/tests/fixtures/component-metadata/div3.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -279,4 +279,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div4.ts b/packages/designer/tests/fixtures/component-metadata/div4.ts index b987e6f842..cbe826fdcd 100644 --- a/packages/designer/tests/fixtures/component-metadata/div4.ts +++ b/packages/designer/tests/fixtures/component-metadata/div4.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -269,4 +269,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div5.ts b/packages/designer/tests/fixtures/component-metadata/div5.ts index bf47a6e855..963d7dd86d 100644 --- a/packages/designer/tests/fixtures/component-metadata/div5.ts +++ b/packages/designer/tests/fixtures/component-metadata/div5.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -280,4 +280,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div6.ts b/packages/designer/tests/fixtures/component-metadata/div6.ts index 47e324a80b..de80a93641 100644 --- a/packages/designer/tests/fixtures/component-metadata/div6.ts +++ b/packages/designer/tests/fixtures/component-metadata/div6.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -280,4 +280,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div7.ts b/packages/designer/tests/fixtures/component-metadata/div7.ts index 33bf012594..b970aa3a7b 100644 --- a/packages/designer/tests/fixtures/component-metadata/div7.ts +++ b/packages/designer/tests/fixtures/component-metadata/div7.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -273,4 +273,4 @@ export default { autoruns: [], }, }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div8.ts b/packages/designer/tests/fixtures/component-metadata/div8.ts index 2686165697..ae04ad287f 100644 --- a/packages/designer/tests/fixtures/component-metadata/div8.ts +++ b/packages/designer/tests/fixtures/component-metadata/div8.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', npm: { @@ -9,4 +9,4 @@ export default { docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs', devMode: 'proCode', tags: ['布局'], -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/div9.ts b/packages/designer/tests/fixtures/component-metadata/div9.ts index 59b3346501..2d3640b3bd 100644 --- a/packages/designer/tests/fixtures/component-metadata/div9.ts +++ b/packages/designer/tests/fixtures/component-metadata/div9.ts @@ -1,8 +1,8 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Div', title: '容器', docUrl: 'https://github.com/alibaba/lowcode-materials/tree/main/docs', devMode: 'proCode', tags: ['布局'], -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/form.ts b/packages/designer/tests/fixtures/component-metadata/form.ts index 34074a9464..faa6e06085 100644 --- a/packages/designer/tests/fixtures/component-metadata/form.ts +++ b/packages/designer/tests/fixtures/component-metadata/form.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Form', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/other.ts b/packages/designer/tests/fixtures/component-metadata/other.ts index e5c644050a..adc8659b8c 100644 --- a/packages/designer/tests/fixtures/component-metadata/other.ts +++ b/packages/designer/tests/fixtures/component-metadata/other.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Other', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/page.ts b/packages/designer/tests/fixtures/component-metadata/page.ts index 40f9d8e388..4170378614 100644 --- a/packages/designer/tests/fixtures/component-metadata/page.ts +++ b/packages/designer/tests/fixtures/component-metadata/page.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Page', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/page2.ts b/packages/designer/tests/fixtures/component-metadata/page2.ts index 40f9d8e388..4170378614 100644 --- a/packages/designer/tests/fixtures/component-metadata/page2.ts +++ b/packages/designer/tests/fixtures/component-metadata/page2.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'Page', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/root-content.ts b/packages/designer/tests/fixtures/component-metadata/root-content.ts index 565c5615d1..5546e8b8a1 100644 --- a/packages/designer/tests/fixtures/component-metadata/root-content.ts +++ b/packages/designer/tests/fixtures/component-metadata/root-content.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'RootContent', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/root-footer.ts b/packages/designer/tests/fixtures/component-metadata/root-footer.ts index 495d6a1eb5..cd3291fb07 100644 --- a/packages/designer/tests/fixtures/component-metadata/root-footer.ts +++ b/packages/designer/tests/fixtures/component-metadata/root-footer.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'RootFooter', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/component-metadata/root-header.ts b/packages/designer/tests/fixtures/component-metadata/root-header.ts index 4d226237b7..b2d3dd4ed7 100644 --- a/packages/designer/tests/fixtures/component-metadata/root-header.ts +++ b/packages/designer/tests/fixtures/component-metadata/root-header.ts @@ -1,4 +1,4 @@ -import { ComponentMetadata } from "@alilc/lowcode-types"; +import { IPublicTypeComponentMetadata } from "@alilc/lowcode-types"; export default { componentName: 'RootHeader', npm: { @@ -276,4 +276,4 @@ export default { ], autoruns: [], }, -} as ComponentMetadata; +} as IPublicTypeComponentMetadata; diff --git a/packages/designer/tests/fixtures/schema/form-with-modal.ts b/packages/designer/tests/fixtures/schema/form-with-modal.ts index 0e0b4bfbd9..4c0fb9064f 100644 --- a/packages/designer/tests/fixtures/schema/form-with-modal.ts +++ b/packages/designer/tests/fixtures/schema/form-with-modal.ts @@ -53,9 +53,9 @@ export default { props: { title: { type: 'i18n', - use: 'zh_CN', - en_US: 'Dialog Title', - zh_CN: 'Dialog标题', + use: 'zh-CN', + 'en-US': 'Dialog Title', + 'zh-CN': 'Dialog标题', }, visible: false, hasMask: true, @@ -66,15 +66,15 @@ export default { footerActions: 'cancel,ok', confirmText: { type: 'i18n', - use: 'zh_CN', - en_US: 'Confirm', - zh_CN: '确定', + use: 'zh-CN', + 'en-US': 'Confirm', + 'zh-CN': '确定', }, cancelText: { type: 'i18n', - use: 'zh_CN', - en_US: 'Cancel', - zh_CN: '取消', + use: 'zh-CN', + 'en-US': 'Cancel', + 'zh-CN': '取消', }, confirmStyle: 'primary', confirmState: '确定', @@ -106,9 +106,9 @@ export default { showTitle: false, behavior: 'NORMAL', content: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '个人信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '个人信息', type: 'i18n', }, __style__: {}, @@ -179,22 +179,22 @@ export default { props: { __slot__title: false, subTitle: { - use: 'zh_CN', - en_US: '', - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, __slot__subTitle: false, extra: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, className: 'card_kgaqfbm5', title: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '基本信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '基本信息', type: 'i18n', }, __slot__extra: false, @@ -243,28 +243,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [ @@ -280,9 +280,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '姓名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '姓名', type: 'i18n', }, __category__: 'form', @@ -290,8 +290,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -302,9 +302,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -317,28 +317,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -350,9 +350,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '英文名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '英文名', type: 'i18n', }, __category__: 'form', @@ -360,8 +360,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -372,9 +372,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -387,28 +387,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -420,9 +420,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '职位', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '职位', type: 'i18n', }, __category__: 'form', @@ -430,8 +430,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -442,9 +442,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -469,28 +469,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -502,9 +502,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '花名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '花名', type: 'i18n', }, __category__: 'form', @@ -512,8 +512,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -524,9 +524,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -538,8 +538,8 @@ export default { fieldName: 'gender', hasClear: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, mode: 'single', @@ -547,9 +547,9 @@ export default { autoWidth: true, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please select', - zh_CN: '请选择', + use: 'zh-CN', + 'en-US': 'please select', + 'zh-CN': '请选择', type: 'i18n', }, hasBorder: true, @@ -563,14 +563,14 @@ export default { __style__: {}, fieldId: 'select_k1ow3h1x', notFoundContent: { - use: 'zh_CN', + use: 'zh-CN', type: 'i18n', }, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'SelectField', - zh_CN: '性别', + use: 'zh-CN', + 'en-US': 'SelectField', + 'zh-CN': '性别', type: 'i18n', }, __category__: 'form', @@ -586,8 +586,8 @@ export default { { defaultChecked: false, text: { - en_US: 'Option 1', - zh_CN: '男', + 'en-US': 'Option 1', + 'zh-CN': '男', type: 'i18n', __sid__: 'param_k1owc4tb', }, @@ -598,8 +598,8 @@ export default { { defaultChecked: false, text: { - en_US: 'Option 2', - zh_CN: '女', + 'en-US': 'Option 2', + 'zh-CN': '女', type: 'i18n', __sid__: 'param_k1owc4tf', }, @@ -613,9 +613,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, searchDelay: 300, }, @@ -635,22 +635,22 @@ export default { props: { __slot__title: false, subTitle: { - use: 'zh_CN', - en_US: '', - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, __slot__subTitle: false, extra: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, className: 'card_kgaqfbm6', title: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '部门信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '部门信息', type: 'i18n', }, __slot__extra: false, @@ -677,28 +677,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -710,9 +710,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '所属部门', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '所属部门', type: 'i18n', }, __category__: 'form', @@ -720,8 +720,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -732,9 +732,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -769,28 +769,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -802,9 +802,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '主管', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '主管', type: 'i18n', }, __category__: 'form', @@ -812,8 +812,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -824,9 +824,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -851,28 +851,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -884,9 +884,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: 'HRG', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': 'HRG', type: 'i18n', }, __category__: 'form', @@ -894,8 +894,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -906,9 +906,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': null, + 'zh-CN': '', }, }, condition: true, @@ -963,9 +963,9 @@ export default { behavior: 'NORMAL', loading: false, content: { - use: 'zh_CN', - en_US: 'Button', - zh_CN: '提交', + use: 'zh-CN', + 'en-US': 'Button', + 'zh-CN': '提交', type: 'i18n', }, __style__: ':root {\n margin-right: 16px;\n width: 80px\n}', @@ -986,9 +986,9 @@ export default { behavior: 'NORMAL', loading: false, content: { - use: 'zh_CN', - en_US: 'Button', - zh_CN: '取消', + use: 'zh-CN', + 'en-US': 'Button', + 'zh-CN': '取消', type: 'i18n', }, __style__: ':root {\n width: 80px;\n}', diff --git a/packages/designer/tests/fixtures/schema/form.ts b/packages/designer/tests/fixtures/schema/form.ts index 6f61170f65..e8479629f8 100644 --- a/packages/designer/tests/fixtures/schema/form.ts +++ b/packages/designer/tests/fixtures/schema/form.ts @@ -70,9 +70,9 @@ export default { showTitle: false, behavior: 'NORMAL', content: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '个人信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '个人信息', type: 'i18n', }, __style__: {}, @@ -143,22 +143,22 @@ export default { props: { __slot__title: false, subTitle: { - use: 'zh_CN', - en_US: '', - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, __slot__subTitle: false, extra: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, className: 'card_kgaqfbm5', title: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '基本信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '基本信息', type: 'i18n', }, __slot__extra: false, @@ -207,28 +207,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [ @@ -244,9 +244,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '姓名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '姓名', type: 'i18n', }, __category__: 'form', @@ -254,8 +254,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -266,9 +266,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -281,28 +281,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -314,9 +314,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '英文名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '英文名', type: 'i18n', }, __category__: 'form', @@ -324,8 +324,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -336,9 +336,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -351,28 +351,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -384,9 +384,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '职位', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '职位', type: 'i18n', }, __category__: 'form', @@ -394,8 +394,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -406,9 +406,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -433,28 +433,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -466,9 +466,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '花名', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '花名', type: 'i18n', }, __category__: 'form', @@ -476,8 +476,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -488,9 +488,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -502,8 +502,8 @@ export default { fieldName: 'gender', hasClear: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, mode: 'single', @@ -511,9 +511,9 @@ export default { autoWidth: true, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please select', - zh_CN: '请选择', + use: 'zh-CN', + 'en-US': 'please select', + 'zh-CN': '请选择', type: 'i18n', }, hasBorder: true, @@ -527,14 +527,14 @@ export default { __style__: {}, fieldId: 'select_k1ow3h1x', notFoundContent: { - use: 'zh_CN', + use: 'zh-CN', type: 'i18n', }, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'SelectField', - zh_CN: '性别', + use: 'zh-CN', + 'en-US': 'SelectField', + 'zh-CN': '性别', type: 'i18n', }, __category__: 'form', @@ -550,8 +550,8 @@ export default { { defaultChecked: false, text: { - en_US: 'Option 1', - zh_CN: '男', + 'en-US': 'Option 1', + 'zh-CN': '男', type: 'i18n', __sid__: 'param_k1owc4tb', }, @@ -562,8 +562,8 @@ export default { { defaultChecked: false, text: { - en_US: 'Option 2', - zh_CN: '女', + 'en-US': 'Option 2', + 'zh-CN': '女', type: 'i18n', __sid__: 'param_k1owc4tf', }, @@ -577,9 +577,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, searchDelay: 300, }, @@ -599,22 +599,22 @@ export default { props: { __slot__title: false, subTitle: { - use: 'zh_CN', - en_US: '', - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, __slot__subTitle: false, extra: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, className: 'card_kgaqfbm6', title: { - use: 'zh_CN', - en_US: 'Title', - zh_CN: '部门信息', + use: 'zh-CN', + 'en-US': 'Title', + 'zh-CN': '部门信息', type: 'i18n', }, __slot__extra: false, @@ -641,28 +641,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -674,9 +674,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '所属部门', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '所属部门', type: 'i18n', }, __category__: 'form', @@ -684,8 +684,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -696,9 +696,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -733,28 +733,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -766,9 +766,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: '主管', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': '主管', type: 'i18n', }, __category__: 'form', @@ -776,8 +776,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -788,9 +788,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -815,28 +815,28 @@ export default { hasClear: false, autoFocus: false, tips: { - en_US: '', - zh_CN: '', + 'en-US': '', + 'zh-CN': '', type: 'i18n', }, trim: false, labelTextAlign: 'right', placeholder: { - use: 'zh_CN', - en_US: 'please input', - zh_CN: '请输入', + use: 'zh-CN', + 'en-US': 'please input', + 'zh-CN': '请输入', type: 'i18n', }, state: '', behavior: 'NORMAL', value: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, addonBefore: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, validation: [], @@ -848,9 +848,9 @@ export default { autoHeight: false, labelColOffset: 0, label: { - use: 'zh_CN', - en_US: 'TextField', - zh_CN: 'HRG', + use: 'zh-CN', + 'en-US': 'TextField', + 'zh-CN': 'HRG', type: 'i18n', }, __category__: 'form', @@ -858,8 +858,8 @@ export default { wrapperColSpan: 0, rows: 4, addonAfter: { - use: 'zh_CN', - zh_CN: '', + use: 'zh-CN', + 'zh-CN': '', type: 'i18n', }, wrapperColOffset: 0, @@ -870,9 +870,9 @@ export default { labelTipsIcon: '', labelTipsText: { type: 'i18n', - use: 'zh_CN', - en_US: null, - zh_CN: '', + use: 'zh-CN', + 'en-US': '', + 'zh-CN': '', }, }, condition: true, @@ -927,9 +927,9 @@ export default { behavior: 'NORMAL', loading: false, content: { - use: 'zh_CN', - en_US: 'Button', - zh_CN: '提交', + use: 'zh-CN', + 'en-US': 'Button', + 'zh-CN': '提交', type: 'i18n', }, __style__: ':root {\n margin-right: 16px;\n width: 80px\n}', @@ -950,9 +950,9 @@ export default { behavior: 'NORMAL', loading: false, content: { - use: 'zh_CN', - en_US: 'Button', - zh_CN: '取消', + use: 'zh-CN', + 'en-US': 'Button', + 'zh-CN': '取消', type: 'i18n', }, __style__: ':root {\n width: 80px;\n}', diff --git a/packages/designer/tests/fixtures/window.ts b/packages/designer/tests/fixtures/window.ts index 6f3e03a884..c57fcb6869 100644 --- a/packages/designer/tests/fixtures/window.ts +++ b/packages/designer/tests/fixtures/window.ts @@ -22,7 +22,7 @@ window.console.warn = () => {}; const originalLog = window.console.log; window.console.log = (...args) => { // suppress boring warnings - if (args[0].includes('@babel/plugin-proposal-private-property-in-object')) return; + if (args[0]?.includes && args[0].includes('@babel/plugin-proposal-private-property-in-object')) return; originalLog.apply(window.console, args); }; window.React = window.React || {}; diff --git a/packages/designer/tests/main/meta/component-meta.test.ts b/packages/designer/tests/main/meta/component-meta.test.ts index a1a113d935..d943f85afd 100644 --- a/packages/designer/tests/main/meta/component-meta.test.ts +++ b/packages/designer/tests/main/meta/component-meta.test.ts @@ -1,5 +1,4 @@ import '../../fixtures/window'; -import { Node } from '../../../src/document/node/node'; import { Designer } from '../../../src/designer/designer'; import divMeta from '../../fixtures/component-metadata/div'; import div2Meta from '../../fixtures/component-metadata/div2'; @@ -19,22 +18,18 @@ import page2Meta from '../../fixtures/component-metadata/page2'; import { ComponentMeta, isComponentMeta, - removeBuiltinComponentAction, - addBuiltinComponentAction, - modifyBuiltinComponentAction, ensureAList, buildFilter, - registerMetadataTransducer, - getRegisteredMetadataTransducers, } from '../../../src/component-meta'; -import { componentDefaults } from '../../../src/transducers'; -const mockCreateSettingEntry = jest.fn(); + jest.mock('../../../src/designer/designer', () => { return { Designer: jest.fn().mockImplementation(() => { + const { ComponentActions } = require('../../../src/component-actions'); return { getGlobalComponentActions: () => [], + componentActions: new ComponentActions(), }; }), }; @@ -126,12 +121,12 @@ describe('组件元数据处理', () => { expect(meta.availableActions[1].name).toBe('hide'); expect(meta.availableActions[2].name).toBe('copy'); - removeBuiltinComponentAction('remove'); + designer.componentActions.removeBuiltinComponentAction('remove'); expect(meta.availableActions).toHaveLength(4); expect(meta.availableActions[0].name).toBe('hide'); expect(meta.availableActions[1].name).toBe('copy'); - addBuiltinComponentAction({ + designer.componentActions.addBuiltinComponentAction({ name: 'new', content: { action() {}, @@ -227,17 +222,17 @@ describe('帮助函数', () => { }); it('registerMetadataTransducer', () => { - expect(getRegisteredMetadataTransducers()).toHaveLength(2); + expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(2); // 插入到 legacy-issues 和 component-defaults 的中间 - registerMetadataTransducer((metadata) => metadata, 3, 'noop'); - expect(getRegisteredMetadataTransducers()).toHaveLength(3); + designer.componentActions.registerMetadataTransducer((metadata) => metadata, 3, 'noop'); + expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(3); - registerMetadataTransducer((metadata) => metadata); - expect(getRegisteredMetadataTransducers()).toHaveLength(4); + designer.componentActions.registerMetadataTransducer((metadata) => metadata); + expect(designer.componentActions.getRegisteredMetadataTransducers()).toHaveLength(4); }); it('modifyBuiltinComponentAction', () => { - modifyBuiltinComponentAction('copy', (action) => { + designer.componentActions.modifyBuiltinComponentAction('copy', (action) => { expect(action.name).toBe('copy'); }); }); diff --git a/packages/designer/tests/plugin/plugin-manager.test.ts b/packages/designer/tests/plugin/plugin-manager.test.ts index b0c40070a3..73915203f0 100644 --- a/packages/designer/tests/plugin/plugin-manager.test.ts +++ b/packages/designer/tests/plugin/plugin-manager.test.ts @@ -1,14 +1,22 @@ import '../fixtures/window'; import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; import { LowCodePluginManager } from '../../src/plugin/plugin-manager'; -import { ILowCodePluginContext, ILowCodePluginManager } from '../../src/plugin/plugin-types'; +import { IPublicModelPluginContext, IPublicApiPlugins } from '@alilc/lowcode-types'; +import { ILowCodePluginContextPrivate } from '../../src/plugin/plugin-types'; const editor = new Editor(); +let contextApiAssembler; describe('plugin 测试', () => { - let pluginManager: ILowCodePluginManager; + let pluginManager: IPublicApiPlugins; beforeEach(() => { - pluginManager = new LowCodePluginManager(editor).toProxy(); + contextApiAssembler = { + assembleApis(context: ILowCodePluginContextPrivate){ + context.plugins = pluginManager as IPublicApiPlugins; + // mock set apis + } + }; + pluginManager = new LowCodePluginManager(contextApiAssembler).toProxy(); }); afterEach(() => { pluginManager.dispose(); @@ -16,7 +24,7 @@ describe('plugin 测试', () => { it('注册插件,插件参数生成函数能被调用,且能拿到正确的 ctx ', () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { mockFn(ctx); return { init: jest.fn(), @@ -40,7 +48,7 @@ describe('plugin 测试', () => { it('注册插件,调用插件 init 方法', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { init: mockFn, exports() { @@ -66,7 +74,7 @@ describe('plugin 测试', () => { it('注册插件,调用 setDisabled 方法', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { init: mockFn, }; @@ -82,7 +90,7 @@ describe('plugin 测试', () => { it('注册插件,调用 plugin.setDisabled 方法', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { init: mockFn, }; @@ -98,7 +106,7 @@ describe('plugin 测试', () => { it('删除插件,调用插件 destroy 方法', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { init: jest.fn(), destroy: mockFn, @@ -116,7 +124,7 @@ describe('plugin 测试', () => { describe('dependencies 依赖', () => { it('dependencies 依赖', async () => { const mockFn = jest.fn(); - const creator21 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo1'), }; @@ -126,7 +134,7 @@ describe('plugin 测试', () => { dependencies: ['demo2'], }; pluginManager.register(creator21); - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo2'), }; @@ -141,7 +149,7 @@ describe('plugin 测试', () => { it('dependencies 依赖 - string', async () => { const mockFn = jest.fn(); - const creator21 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo1'), }; @@ -151,7 +159,7 @@ describe('plugin 测试', () => { dependencies: 'demo2', }; pluginManager.register(creator21); - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo2'), }; @@ -166,7 +174,7 @@ describe('plugin 测试', () => { it('dependencies 依赖 - 兼容 dep', async () => { const mockFn = jest.fn(); - const creator21 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: IPublicModelPluginContext) => { return { dep: ['demo4'], init: () => mockFn('demo3'), @@ -174,7 +182,7 @@ describe('plugin 测试', () => { }; creator21.pluginName = 'demo3'; pluginManager.register(creator21); - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo4'), }; @@ -189,7 +197,7 @@ describe('plugin 测试', () => { it('dependencies 依赖 - 兼容 dep & string', async () => { const mockFn = jest.fn(); - const creator21 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: IPublicModelPluginContext) => { return { dep: 'demo4', init: () => mockFn('demo3'), @@ -197,7 +205,7 @@ describe('plugin 测试', () => { }; creator21.pluginName = 'demo3'; pluginManager.register(creator21); - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo4'), }; @@ -213,7 +221,7 @@ describe('plugin 测试', () => { it('version 依赖', async () => { const mockFn = jest.fn(); - const creator21 = (ctx: ILowCodePluginContext) => { + const creator21 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo1'), }; @@ -238,7 +246,7 @@ describe('plugin 测试', () => { expect(pluginManager.plugins.length).toBe(0); - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo2'), }; @@ -254,7 +262,7 @@ describe('plugin 测试', () => { pluginManager.register(creator22); expect(pluginManager.plugins.length).toBe(1); - const creator23 = (ctx: ILowCodePluginContext) => { + const creator23 = (ctx: IPublicModelPluginContext) => { return { init: () => mockFn('demo3'), }; @@ -272,7 +280,7 @@ describe('plugin 测试', () => { it('autoInit 功能', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { init: mockFn, }; @@ -284,7 +292,7 @@ describe('plugin 测试', () => { it('插件不会重复 init,除非强制重新 init', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { name: 'demo1', init: mockFn, @@ -304,7 +312,7 @@ describe('plugin 测试', () => { it('默认情况不允许重复注册', async () => { const mockFn = jest.fn(); - const mockPlugin = (ctx: ILowCodePluginContext) => { + const mockPlugin = (ctx: IPublicModelPluginContext) => { return { init: mockFn, }; @@ -319,7 +327,7 @@ describe('plugin 测试', () => { it('插件增加 override 参数时可以重复注册', async () => { const mockFn = jest.fn(); - const mockPlugin = (ctx: ILowCodePluginContext) => { + const mockPlugin = (ctx: IPublicModelPluginContext) => { return { init: mockFn, }; @@ -333,7 +341,7 @@ describe('plugin 测试', () => { it('插件增加 override 参数时可以重复注册, 被覆盖的如果已初始化,会被销毁', async () => { const mockInitFn = jest.fn(); const mockDestroyFn = jest.fn(); - const mockPlugin = (ctx: ILowCodePluginContext) => { + const mockPlugin = (ctx: IPublicModelPluginContext) => { return { init: mockInitFn, destroy: mockDestroyFn, @@ -347,33 +355,8 @@ describe('plugin 测试', () => { await pluginManager.init(); }); - it('内部事件机制', async () => { - const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { - return {}; - }; - creator2.pluginName = 'demo1'; - pluginManager.register(creator2); - await pluginManager.init(); - const plugin = pluginManager.get('demo1')!; - - const off = plugin.on('haha', mockFn); - plugin.emit('haha', 1, 2, 3); - - expect(mockFn).toHaveBeenCalledTimes(1); - expect(mockFn).toHaveBeenCalledWith(1, 2, 3); - - off(); - plugin.emit('haha', 1, 2, 3); - expect(mockFn).toHaveBeenCalledTimes(1); - - plugin.removeAllListeners('haha'); - plugin.emit('haha', 1, 2, 3); - expect(mockFn).toHaveBeenCalledTimes(1); - }); - it('dispose 方法', async () => { - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return {}; }; creator2.pluginName = 'demo1'; @@ -386,7 +369,7 @@ describe('plugin 测试', () => { }); it('getAll 方法', async () => { - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return {}; }; creator2.pluginName = 'demo1'; @@ -397,7 +380,7 @@ describe('plugin 测试', () => { }); it('getPluginPreference 方法 - null', async () => { - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return {}; }; creator2.pluginName = 'demo1'; @@ -408,7 +391,7 @@ describe('plugin 测试', () => { }); it('getPluginPreference 方法', async () => { - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return {}; }; const preference = new Map(); @@ -432,7 +415,7 @@ describe('plugin 测试', () => { key5: 'value for key5, but declared, should not work', }); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { mockFnForCtx(ctx); return { init: jest.fn(), @@ -466,7 +449,7 @@ describe('plugin 测试', () => { ], }, }; - const creator22 = (ctx: ILowCodePluginContext) => { + const creator22 = (ctx: IPublicModelPluginContext) => { mockFnForCtx2(ctx); return { init: jest.fn(), @@ -515,7 +498,7 @@ describe('plugin 测试', () => { it('注册插件,没有填写 pluginName,默认值为 anonymous', async () => { const mockFn = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { return { name: 'xxx', init: () => mockFn('anonymous'), @@ -529,7 +512,7 @@ describe('plugin 测试', () => { const mockFn = jest.fn(); const mockFn2 = jest.fn(); - const creator2 = (ctx: ILowCodePluginContext) => { + const creator2 = (ctx: IPublicModelPluginContext) => { mockFn2(ctx); return { init: () => mockFn('anonymous'), diff --git a/packages/designer/tests/plugin/sequencify.test.ts b/packages/designer/tests/plugin/sequencify.test.ts new file mode 100644 index 0000000000..89140e2794 --- /dev/null +++ b/packages/designer/tests/plugin/sequencify.test.ts @@ -0,0 +1,128 @@ +import sequencify, { sequence } from '../../src/plugin/sequencify'; + +describe('sequence', () => { + it('handles tasks with no dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] }, + task2: { name: 'Task 2', dep: [] } + }; + const results = []; + const missing = []; + const recursive = []; + sequence({ tasks, names: ['task1', 'task2'], results, missing, recursive, nest: [] }); + + expect(results).toEqual(['task1', 'task2']); + expect(missing).toEqual([]); + expect(recursive).toEqual([]); + }); + + it('correctly orders tasks based on dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] }, + task2: { name: 'Task 2', dep: ['task1'] } + }; + const results = []; + const missing = []; + const recursive = []; + sequence({ tasks, names: ['task2', 'task1'], results, missing, recursive, nest: [] }); + + expect(results).toEqual(['task1', 'task2']); + expect(missing).toEqual([]); + expect(recursive).toEqual([]); + }); + + it('identifies missing tasks', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] } + }; + const results = []; + const missing = []; + const recursive = []; + const nest = [] + sequence({ tasks, names: ['task2'], results, missing, recursive, nest }); + + expect(results).toEqual(['task2']); + expect(missing).toEqual(['task2']); + expect(recursive).toEqual([]); + expect(nest).toEqual([]); + }); + + it('detects recursive dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: ['task2'] }, + task2: { name: 'Task 2', dep: ['task1'] } + }; + const results = []; + const missing = []; + const recursive = []; + const nest = [] + sequence({ tasks, names: ['task1', 'task2'], results, missing, recursive, nest }); + + expect(results).toEqual(['task1', 'task2', 'task1']); + expect(missing).toEqual([]); + expect(recursive).toEqual([['task1', 'task2', 'task1']]); + expect(nest).toEqual([]); + }); +}); + +describe('sequence', () => { + + it('should return tasks in sequence without dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] }, + task2: { name: 'Task 2', dep: [] }, + task3: { name: 'Task 3', dep: [] } + }; + const names = ['task1', 'task2', 'task3']; + const expected = { + sequence: ['task1', 'task2', 'task3'], + missingTasks: [], + recursiveDependencies: [] + }; + expect(sequencify(tasks, names)).toEqual(expected); + }); + + it('should handle tasks with dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] }, + task2: { name: 'Task 2', dep: ['task1'] }, + task3: { name: 'Task 3', dep: ['task2'] } + }; + const names = ['task3', 'task2', 'task1']; + const expected = { + sequence: ['task1', 'task2', 'task3'], + missingTasks: [], + recursiveDependencies: [] + }; + expect(sequencify(tasks, names)).toEqual(expected); + }); + + it('should identify missing tasks', () => { + const tasks = { + task1: { name: 'Task 1', dep: [] }, + task2: { name: 'Task 2', dep: ['task3'] } // task3 is missing + }; + const names = ['task1', 'task2']; + const expected = { + sequence: [], + missingTasks: ['task2.task3'], + recursiveDependencies: [] + }; + expect(sequencify(tasks, names)).toEqual(expected); + }); + + it('should detect recursive dependencies', () => { + const tasks = { + task1: { name: 'Task 1', dep: ['task2'] }, + task2: { name: 'Task 2', dep: ['task1'] } // Recursive dependency + }; + const names = ['task1', 'task2']; + const expected = { + sequence: [], + missingTasks: [], + recursiveDependencies: [['task1', 'task2', 'task1']] + }; + expect(sequencify(tasks, names)).toEqual(expected); + }); + +}); \ No newline at end of file diff --git a/packages/designer/tests/project/project-methods.test.ts b/packages/designer/tests/project/project-methods.test.ts index 0e545e4f4e..c710b29f13 100644 --- a/packages/designer/tests/project/project-methods.test.ts +++ b/packages/designer/tests/project/project-methods.test.ts @@ -1,13 +1,10 @@ -import set from 'lodash/set'; -import cloneDeep from 'lodash/cloneDeep'; import '../fixtures/window'; import { Editor } from '@alilc/lowcode-editor-core'; import { Project } from '../../src/project/project'; import { DocumentModel } from '../../src/document/document-model'; -import { Node } from '../../src/document/node/node'; import { Designer } from '../../src/designer/designer'; import formSchema from '../fixtures/schema/form'; -import { getIdsFromSchema, getNodeFromSchemaById } from '../utils'; +import { shellModelFactory } from '../../../engine/src/modules/shell-model-factory'; describe.only('Project 方法测试', () => { let editor: Editor; @@ -17,7 +14,7 @@ describe.only('Project 方法测试', () => { beforeEach(() => { editor = new Editor(); - designer = new Designer({ editor }); + designer = new Designer({ editor, shellModelFactory }); project = designer.project; doc = new DocumentModel(project, formSchema); }); @@ -148,9 +145,9 @@ describe.only('Project 方法测试', () => { it('simulatorProps', () => { designer._simulatorProps = { a: 1 }; - expect(project.simulatorProps.a).toBe(1); + expect(designer.simulatorProps.a).toBe(1); designer._simulatorProps = () => ({ a: 1 }); - expect(project.simulatorProps.a).toBe(1); + expect(designer.simulatorProps.a).toBe(1); }); it('onCurrentDocumentChange', () => { diff --git a/packages/designer/tests/project/project.test.ts b/packages/designer/tests/project/project.test.ts index 1a5930c6a3..2066c03985 100644 --- a/packages/designer/tests/project/project.test.ts +++ b/packages/designer/tests/project/project.test.ts @@ -17,6 +17,9 @@ jest.mock('../../src/designer/designer', () => { getMetadata() { return { configure: { advanced: null } }; }, + get advanced() { + return {}; + }, }; }, transformProps(props) { return props; }, @@ -255,12 +258,12 @@ describe('schema 生成节点模型测试', () => { expect(project).toBeTruthy(); project.i18n = formSchema.i18n; - expect(project.i18n).toBe(formSchema.i18n); + expect(project.i18n).toStrictEqual(formSchema.i18n); project.i18n = null; expect(project.i18n).toStrictEqual({}); project.set('i18n', formSchema.i18n); - expect(project.get('i18n')).toBe(formSchema.i18n); + expect(project.get('i18n')).toStrictEqual(formSchema.i18n); project.set('i18n', null); expect(project.get('i18n')).toStrictEqual({}); }); diff --git a/packages/designer/tsconfig.json b/packages/designer/tsconfig.json new file mode 100644 index 0000000000..9136085c95 --- /dev/null +++ b/packages/designer/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib", + "types": ["node","jest"] + }, + "include": [ + "./src/", + "./tests/" + ], + "exclude": ["**/lib", "**/es", "node_modules"] +} diff --git a/packages/editor-core/build.json b/packages/editor-core/build.json index 40b17de79a..c1ebc2c867 100644 --- a/packages/editor-core/build.json +++ b/packages/editor-core/build.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "build-plugin-fusion", "./build.plugin.js" ] 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('|')})$`] = '<rootDir>/../$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 e55c0c2a92..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.0.15", + "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 --skip-demo" + "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.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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", @@ -25,8 +27,7 @@ "power-di": "^2.2.4", "react": "^16", "react-dom": "^16.7.0", - "store": "^2.0.12", - "zen-logger": "^1.1.0" + "store": "^2.0.12" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", @@ -37,7 +38,6 @@ "@types/react": "^16", "@types/react-dom": "^16", "@types/store": "^2.0.2", - "build-plugin-component": "^0.2.11", "build-plugin-fusion": "^0.1.0", "build-plugin-moment-locales": "^0.1.0" }, @@ -49,5 +49,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/editor-core" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } 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<IPublicApiCommand, 'registerCommand' | 'batchExecuteCommand'> { + 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<string, IPublicTypeCommand> = 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/config.ts b/packages/editor-core/src/config.ts index b88dda6e65..c4ff407b9a 100644 --- a/packages/editor-core/src/config.ts +++ b/packages/editor-core/src/config.ts @@ -1,10 +1,12 @@ -import { ComponentType } from 'react'; import { get as lodashGet } from 'lodash'; import { isPlainObject } from '@alilc/lowcode-utils'; - -import { RequestHandlersMap } from '@alilc/lowcode-datasource-types'; - +import { + IPublicTypeEngineOptions, + IPublicModelEngineConfig, + IPublicModelPreference, +} from '@alilc/lowcode-types'; import { getLogger } from './utils/logger'; +import Preference from './utils/preference'; const logger = getLogger({ level: 'log', bizName: 'config' }); @@ -37,12 +39,12 @@ const VALID_ENGINE_OPTIONS = { }, locale: { type: 'string', - default: 'zh_CN', + default: 'zh-CN', description: '语言', }, renderEnv: { type: 'string', - enum: ['react', 'rax', 'any string value'], + enum: ['react', 'any string value'], default: 'react', description: '渲染器类型', }, @@ -53,7 +55,7 @@ const VALID_ENGINE_OPTIONS = { enableStrictPluginMode: { type: 'boolean', default: STRICT_PLUGIN_MODE_DEFAULT, - description: '开启严格插件模式,默认值: STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过engineOptions传递自定义配置项', + description: '开启严格插件模式,默认值:STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过 engineOptions 传递自定义配置项', }, enableReactiveContainer: { type: 'boolean', @@ -122,9 +124,7 @@ const VALID_ENGINE_OPTIONS = { type: 'array', description: '自定义 simulatorUrl 的地址', }, - /** - * 与 react-renderer 的 appHelper 一致, https://lowcode-engine.cn/docV2/nhilce#appHelper - */ + // 与 react-renderer 的 appHelper 一致,https://lowcode-engine.cn/site/docs/guide/expand/runtime/renderer#apphelper appHelper: { type: 'object', description: '定义 utils 和 constants 等对象', @@ -145,141 +145,33 @@ const VALID_ENGINE_OPTIONS = { type: 'function', description: '配置指定节点为根组件', }, + enableAutoOpenFirstWindow: { + type: 'boolean', + description: '应用级设计模式下,自动打开第一个窗口', + default: true, + }, + enableWorkspaceMode: { + type: 'boolean', + description: '是否开启应用级设计模式', + default: false, + }, + workspaceEmptyComponent: { + type: 'function', + description: '应用级设计模式下,窗口为空时展示的占位组件', + }, + enableContextMenu: { + type: 'boolean', + description: '是否开启右键菜单', + default: false, + }, + hideComponentAction: { + type: 'boolean', + description: '是否隐藏设计器辅助层', + default: false, + }, }; -export interface EngineOptions { - /** - * 是否开启 condition 的能力,默认在设计器中不管 condition 是啥都正常展示 - */ - enableCondition?: boolean; - /** - * @todo designMode 无法映射到文档渲染模块 - * - * 设计模式,live 模式将会实时展示变量值,默认值:'design' - */ - designMode?: 'design' | 'live'; - /** - * 设备类型,默认值:'default' - */ - device?: 'default' | 'mobile' | string; - /** - * 指定初始化的 deviceClassName,挂载到画布的顶层节点上 - */ - deviceClassName?: string; - /** - * 语言,默认值:'zh_CN' - */ - locale?: string; - /** - * 渲染器类型,默认值:'react' - */ - renderEnv?: 'react' | 'rax' | string; - /** - * 设备类型映射器,处理设计器与渲染器中 device 的映射 - */ - deviceMapper?: { - transform: (originalDevice: string) => string; - }; - /** - * 开启严格插件模式,默认值: STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过engineOptions传递自定义配置项 - * enable strict plugin mode, default value: false - * under strict mode, customed engineOption is not accepted. - */ - enableStrictPluginMode?: boolean; - /** - * 开启拖拽组件时,即将被放入的容器是否有视觉反馈,默认值:false - */ - enableReactiveContainer?: boolean; - /** - * 关闭画布自动渲染,在资产包多重异步加载的场景有效,默认值:false - */ - disableAutoRender?: boolean; - /** - * 关闭拖拽组件时的虚线响应,性能考虑,默认值:false - */ - disableDetecting?: boolean; - /** - * 定制画布中点击被忽略的 selectors,默认值:undefined - */ - customizeIgnoreSelectors?: (defaultIgnoreSelectors: string[], e: MouseEvent) => string[]; - /** - * 禁止默认的设置面板,默认值:false - */ - disableDefaultSettingPanel?: boolean; - /** - * 禁止默认的设置器,默认值:false - */ - disableDefaultSetters?: boolean; - /** - * 打开画布的锁定操作,默认值:false - */ - enableCanvasLock?: boolean; - /** - * 容器锁定后,容器本身是否可以设置属性,仅当画布锁定特性开启时生效, 默认值为:false - */ - enableLockedNodeSetting?: boolean; - /** - * 当选中节点切换时,是否停留在相同的设置 tab 上,默认值:false - */ - stayOnTheSameSettingTab?: boolean; - /** - * 是否在只有一个 item 的时候隐藏设置 tabs,默认值:false - */ - hideSettingsTabsWhenOnlyOneItem?: boolean; - /** - * 自定义 loading 组件 - */ - loadingComponent?: ComponentType; - /** - * 设置所有属性支持变量配置,默认值:false - */ - supportVariableGlobally?: boolean; - /** - * 设置 simulator 相关的 url,默认值:undefined - */ - simulatorUrl?: string[]; - /** - * Vision-polyfill settings - */ - visionSettings?: { - // 是否禁用降级 reducer,默认值:false - disableCompatibleReducer?: boolean; - // 是否开启在 render 阶段开启 filter reducer,默认值:false - enableFilterReducerInRenderStage?: boolean; - }; - /** - * 与 react-renderer 的 appHelper 一致, https://lowcode-engine.cn/docV2/nhilce#appHelper - */ - appHelper?: { - /** 全局公共函数 */ - utils?: Record<string, any>; - /** 全局常量 */ - constants?: Record<string, any>; - }; - - /** - * 数据源引擎的请求处理器映射 - */ - requestHandlersMap?: RequestHandlersMap; - - /** - * @default true - * JSExpression 是否只支持使用 this 来访问上下文变量,假如需要兼容原来的 'state.xxx',则设置为 false - */ - thisRequiredInJSE?: boolean; - - /** - * @default false - * 当开启组件未找到严格模式时,渲染模块不会默认给一个容器组件 - */ - enableStrictNotFoundMode?: boolean; - /** - * 配置指定节点为根组件 - */ - focusNodeSelector?: (rootNode: Node) => Node; -} - -const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean): boolean => { +const getStrictModeValue = (engineOptions: IPublicTypeEngineOptions, defaultValue: boolean): boolean => { if (!engineOptions || !isPlainObject(engineOptions)) { return defaultValue; } @@ -289,7 +181,24 @@ const getStrictModeValue = (engineOptions: EngineOptions, defaultValue: boolean) } return engineOptions.enableStrictPluginMode; }; -export class EngineConfig { + +export interface IEngineConfig extends IPublicModelEngineConfig { + + /** + * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions. + * + * @param {IPublicTypeEngineOptions} engineOptions + */ + setEngineOptions(engineOptions: IPublicTypeEngineOptions): void; + + notifyGot(key: string): void; + + setWait(key: string, resolve: (data: any) => void, once?: boolean): void; + + delWait(key: string, fn: any): void; +} + +export class EngineConfig implements IEngineConfig { private config: { [key: string]: any } = {}; private waits = new Map< @@ -300,14 +209,20 @@ export class EngineConfig { }> >(); + /** + * used to store preferences + * + */ + readonly preference: IPublicModelPreference; + constructor(config?: { [key: string]: any }) { this.config = config || {}; + this.preference = new Preference(); } /** * 判断指定 key 是否有值 * @param key - * @returns */ has(key: string): boolean { return this.config[key] !== undefined; @@ -317,7 +232,6 @@ export class EngineConfig { * 获取指定 key 的值 * @param key * @param defaultValue - * @returns */ get(key: string, defaultValue?: any): any { return lodashGet(this.config, key, defaultValue); @@ -348,10 +262,9 @@ export class EngineConfig { /** * if engineOptions.strictPluginMode === true, only accept propertied predefined in EngineOptions. * - * @param {EngineOptions} engineOptions - * @memberof EngineConfig + * @param {IPublicTypeEngineOptions} engineOptions */ - setEngineOptions(engineOptions: EngineOptions) { + setEngineOptions(engineOptions: IPublicTypeEngineOptions) { if (!engineOptions || !isPlainObject(engineOptions)) { return; } @@ -363,7 +276,7 @@ export class EngineConfig { }; Object.keys(engineOptions).forEach((key) => { if (isValidKey(key)) { - this.set(key, engineOptions[key]); + this.set(key, (engineOptions as any)[key]); } else { logger.warn(`failed to config ${key} to engineConfig, only predefined options can be set under strict mode, predefined options: `, VALID_ENGINE_OPTIONS); } @@ -399,16 +312,14 @@ export class EngineConfig { const val = this.config?.[key]; if (val !== undefined) { fn(val); - return () => {}; - } else { - this.setWait(key, fn); - return () => { - this.delWait(key, fn); - }; } + this.setWait(key, fn); + return () => { + this.delWait(key, fn); + }; } - private notifyGot(key: string) { + notifyGot(key: string): void { let waits = this.waits.get(key); if (!waits) { return; @@ -428,7 +339,7 @@ export class EngineConfig { } } - private setWait(key: string, resolve: (data: any) => void, once?: boolean) { + setWait(key: string, resolve: (data: any) => void, once?: boolean) { const waits = this.waits.get(key); if (waits) { waits.push({ resolve, once }); @@ -437,7 +348,7 @@ export class EngineConfig { } } - private delWait(key: string, fn: any) { + delWait(key: string, fn: any) { const waits = this.waits.get(key); if (!waits) { return; @@ -452,6 +363,10 @@ export class EngineConfig { this.waits.delete(key); } } + + getPreference(): IPublicModelPreference { + return this.preference; + } } export const engineConfig = new EngineConfig(); diff --git a/packages/editor-core/src/di/setter.ts b/packages/editor-core/src/di/setter.ts index 2dd350deb4..5af2c0230f 100644 --- a/packages/editor-core/src/di/setter.ts +++ b/packages/editor-core/src/di/setter.ts @@ -1,29 +1,13 @@ import { ReactNode } from 'react'; -import { CustomView, isCustomView, TitleContent } from '@alilc/lowcode-types'; -import { createContent } from '@alilc/lowcode-utils'; +import { IPublicApiSetters, IPublicModelSettingField, IPublicTypeCustomView, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; +import { createContent, isCustomView } from '@alilc/lowcode-utils'; -export type RegisteredSetter = { - component: CustomView; - defaultProps?: object; - title?: TitleContent; - /** - * for MixedSetter to check this setter if available - */ - condition?: (field: any) => boolean; - /** - * for MixedSetter to manual change to this setter - */ - initialValue?: any | ((field: any) => any); - recommend?: boolean; - // 标识是否为动态setter,默认为true - isDynamic?: boolean; -}; -const settersMap = new Map<string, RegisteredSetter & { +const settersMap = new Map<string, IPublicTypeRegisteredSetter & { type: string; }>(); export function registerSetter( - typeOrMaps: string | { [key: string]: CustomView | RegisteredSetter }, - setter?: CustomView | RegisteredSetter, + typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, + setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter, ) { if (typeof typeOrMaps === 'object') { Object.keys(typeOrMaps).forEach(type => { @@ -44,7 +28,7 @@ export function registerSetter( if (!setter.initialValue) { const initial = getInitialFromSetter(setter.component); if (initial) { - setter.initialValue = (field: any) => { + setter.initialValue = (field: IPublicModelSettingField) => { return initial.call(field, field.getValue()); }; } @@ -59,32 +43,76 @@ function getInitialFromSetter(setter: any) { ) || null; // eslint-disable-line } -export function getSetter(type: string): RegisteredSetter | null { - return settersMap.get(type) || null; -} -export function getSettersMap() { - return settersMap; +export interface ISetters extends IPublicApiSetters { + } -export function createSetterContent(setter: any, props: Record<string, any>): ReactNode { - if (typeof setter === 'string') { - setter = getSetter(setter); +export class Setters implements ISetters { + settersMap = new Map<string, IPublicTypeRegisteredSetter & { + type: string; + }>(); + + constructor(readonly viewName: string = 'global') {} + + getSetter = (type: string): IPublicTypeRegisteredSetter | null => { + return this.settersMap.get(type) || null; + }; + + registerSetter = ( + typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, + setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter, + ) => { + if (typeof typeOrMaps === 'object') { + Object.keys(typeOrMaps).forEach(type => { + this.registerSetter(type, typeOrMaps[type]); + }); + return; + } if (!setter) { - return null; + return; } - if (setter.defaultProps) { - props = { - ...setter.defaultProps, - ...props, + if (isCustomView(setter)) { + setter = { + component: setter, + // todo: intl + title: (setter as any).displayName || (setter as any).name || 'CustomSetter', }; } - setter = setter.component; - } + if (!setter.initialValue) { + const initial = getInitialFromSetter(setter.component); + if (initial) { + setter.initialValue = (field: IPublicModelSettingField) => { + return initial.call(field, field.getValue()); + }; + } + } + this.settersMap.set(typeOrMaps, { type: typeOrMaps, ...setter }); + }; - // Fusion的表单组件都是通过 'value' in props 来判断是否使用 defaultValue - if ('value' in props && typeof props.value === 'undefined') { - delete props.value; - } + getSettersMap = () => { + return this.settersMap; + }; - return createContent(setter, props); -} + createSetterContent = (setter: any, props: Record<string, any>): ReactNode => { + if (typeof setter === 'string') { + setter = this.getSetter(setter); + if (!setter) { + return null; + } + if (setter.defaultProps) { + props = { + ...setter.defaultProps, + ...props, + }; + } + setter = setter.component; + } + + // Fusion 的表单组件都是通过 'value' in props 来判断是否使用 defaultValue + if ('value' in props && typeof props.value === 'undefined') { + delete props.value; + } + + return createContent(setter, props); + }; +} \ No newline at end of file diff --git a/packages/editor-core/src/editor.ts b/packages/editor-core/src/editor.ts index bc46086f65..f31a2a2dda 100644 --- a/packages/editor-core/src/editor.ts +++ b/packages/editor-core/src/editor.ts @@ -1,22 +1,24 @@ +/* eslint-disable no-console */ +/* eslint-disable max-len */ import { StrictEventEmitter } from 'strict-event-emitter-types'; import { EventEmitter } from 'events'; +import { EventBus, IEventBus } from './event-bus'; import { - IEditor, + IPublicModelEditor, EditorConfig, PluginClassSet, - KeyType, - GetReturnType, + IPublicTypeEditorValueKey, + IPublicTypeEditorGetResult, HookConfig, - ComponentDescription, - RemoteComponentDescription, + IPublicTypeComponentDescription, + IPublicTypeRemoteComponentDescription, GlobalEvent, } from '@alilc/lowcode-types'; import { engineConfig } from './config'; import { globalLocale } from './intl'; -import * as utils from './utils'; -import Preference from './utils/preference'; import { obx } from './utils'; -import { AssetsJson, AssetLoader } from '@alilc/lowcode-utils'; +import { IPublicTypeAssetsJson, AssetLoader } from '@alilc/lowcode-utils'; +import { assetsTransform } from './utils/assets-transform'; EventEmitter.defaultMaxListeners = 100; @@ -27,8 +29,16 @@ const keyBlacklist = [ 'currentDocument', 'simulator', 'plugins', + 'setters', + 'material', + 'innerHotkey', + 'innerPlugins', ]; +const AssetsCache: { + [key: string]: IPublicTypeRemoteComponentDescription; +} = {}; + export declare interface Editor extends StrictEventEmitter<EventEmitter, GlobalEvent.EventConfig> { addListener(event: string | symbol, listener: (...args: any[]) => void): this; once(event: string | symbol, listener: (...args: any[]) => void): this; @@ -44,39 +54,67 @@ export declare interface Editor extends StrictEventEmitter<EventEmitter, GlobalE prependListener(event: string | symbol, listener: (...args: any[]) => void): this; prependOnceListener(event: string | symbol, listener: (...args: any[]) => void): this; eventNames(): Array<string | symbol>; - getPreference(): Preference; +} + +export interface IEditor extends IPublicModelEditor { + config?: EditorConfig; + + components?: PluginClassSet; + + eventBus: IEventBus; + + init(config?: EditorConfig, components?: PluginClassSet): Promise<any>; } // eslint-disable-next-line no-redeclare -export class Editor extends (EventEmitter as any) implements IEditor { +export class Editor extends EventEmitter implements IEditor { + /** * Ioc Container */ - @obx.shallow private context = new Map<KeyType, any>(); + @obx.shallow private context = new Map<IPublicTypeEditorValueKey, any>(); get locale() { return globalLocale.getLocale(); } + config?: EditorConfig; + + eventBus: EventBus; + + components?: PluginClassSet; + // readonly utils = utils; - /** - * used to store preferences - * - * @memberof Editor - */ - readonly preference = new Preference(); private hooks: HookConfig[] = []; - get<T = undefined, KeyOrType = any>(keyOrType: KeyOrType): GetReturnType<T, KeyOrType> | undefined { + private waits = new Map< + IPublicTypeEditorValueKey, + Array<{ + once?: boolean; + resolve: (data: any) => void; + }> + >(); + + constructor(readonly viewName: string = 'global', readonly workspaceMode: boolean = false) { + // eslint-disable-next-line constructor-super + super(); + // set global emitter maxListeners + this.setMaxListeners(200); + this.eventBus = new EventBus(this); + } + + get<T = undefined, KeyOrType = any>( + keyOrType: KeyOrType, + ): IPublicTypeEditorGetResult<T, KeyOrType> | undefined { return this.context.get(keyOrType as any); } - has(keyOrType: KeyType): boolean { + has(keyOrType: IPublicTypeEditorValueKey): boolean { return this.context.has(keyOrType); } - set(key: KeyType, data: any): void | Promise<void> { + set(key: IPublicTypeEditorValueKey, data: any): void | Promise<void> { if (key === 'assets') { return this.setAssets(data); } @@ -88,11 +126,11 @@ export class Editor extends (EventEmitter as any) implements IEditor { this.notifyGot(key); } - async setAssets(assets: AssetsJson) { + async setAssets(assets: IPublicTypeAssetsJson) { const { components } = assets; if (components && components.length) { - const componentDescriptions: ComponentDescription[] = []; - const remoteComponentDescriptions: RemoteComponentDescription[] = []; + const componentDescriptions: IPublicTypeComponentDescription[] = []; + const remoteComponentDescriptions: IPublicTypeRemoteComponentDescription[] = []; components.forEach((component: any) => { if (!component) { return; @@ -109,23 +147,68 @@ export class Editor extends (EventEmitter as any) implements IEditor { // 如果有远程组件描述协议,则自动加载并补充到资产包中,同时出发 designer.incrementalAssetsReady 通知组件面板更新数据 if (remoteComponentDescriptions && remoteComponentDescriptions.length) { await Promise.all( - remoteComponentDescriptions.map(async (component: any) => { - const { exportName, url } = component; - await (new AssetLoader()).load(url); - if (window[exportName]) { - assets.components = assets.components.concat(window[exportName].components || []); - assets.componentList = assets.componentList.concat(window[exportName].componentList || []); + remoteComponentDescriptions.map(async (component: IPublicTypeRemoteComponentDescription) => { + const { exportName, url, npm } = component; + if (!url || !exportName) { + return; + } + if (!AssetsCache[exportName] || !npm?.version || AssetsCache[exportName].npm?.version !== npm?.version) { + await (new AssetLoader()).load(url); + } + AssetsCache[exportName] = component; + function setAssetsComponent(component: any, extraNpmInfo: any = {}) { + const components = component.components; + assets.componentList = assets.componentList?.concat(component.componentList || []); + if (Array.isArray(components)) { + components.forEach(d => { + assets.components = assets.components.concat({ + npm: { + ...npm, + ...extraNpmInfo, + }, + ...d, + } || []); + }); + return; + } + if (component.components) { + assets.components = assets.components.concat({ + npm: { + ...npm, + ...extraNpmInfo, + }, + ...component.components, + } || []); + } + } + function setArrayAssets(value: any[], preExportName: string = '', preSubName: string = '') { + value.forEach((d: any, i: number) => { + const exportName = [preExportName, i.toString()].filter(d => !!d).join('.'); + const subName = [preSubName, i.toString()].filter(d => !!d).join('.'); + Array.isArray(d) ? setArrayAssets(d, exportName, subName) : setAssetsComponent(d, { + exportName, + subName, + }); + }); + } + if ((window as any)[exportName]) { + if (Array.isArray((window as any)[exportName])) { + setArrayAssets((window as any)[exportName] as any); + } else { + setAssetsComponent((window as any)[exportName] as any); + } } - return window[exportName]; + return (window as any)[exportName]; }), ); } } - this.context.set('assets', assets); + const innerAssets = assetsTransform(assets); + this.context.set('assets', innerAssets); this.notifyGot('assets'); } - onceGot<T = undefined, KeyOrType extends KeyType = any>(keyOrType: KeyOrType): Promise<GetReturnType<T, KeyOrType>> { + onceGot<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>(keyOrType: KeyOrType): Promise<IPublicTypeEditorGetResult<T, KeyOrType>> { const x = this.context.get(keyOrType); if (x !== undefined) { return Promise.resolve(x); @@ -135,38 +218,42 @@ export class Editor extends (EventEmitter as any) implements IEditor { }); } - onGot<T = undefined, KeyOrType extends KeyType = any>( + onGot<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( keyOrType: KeyOrType, - fn: (data: GetReturnType<T, KeyOrType>) => void, + fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void, ): () => void { const x = this.context.get(keyOrType); if (x !== undefined) { fn(x); - return () => {}; - } else { - this.setWait(keyOrType, fn); - return () => { - this.delWait(keyOrType, fn); - }; } + this.setWait(keyOrType, fn); + return () => { + this.delWait(keyOrType, fn); + }; + } + + onChange<T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( + keyOrType: KeyOrType, + fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void, + ): () => void { + this.setWait(keyOrType, fn); + return () => { + this.delWait(keyOrType, fn); + }; } - register(data: any, key?: KeyType): void { + register(data: any, key?: IPublicTypeEditorValueKey): void { this.context.set(key || data, data); this.notifyGot(key || data); } - config?: EditorConfig; - - components?: PluginClassSet; - async init(config?: EditorConfig, components?: PluginClassSet): Promise<any> { this.config = config || {}; this.components = components || {}; const { hooks = [], lifeCycles } = this.config; this.emit('editor.beforeInit'); - const init = (lifeCycles && lifeCycles.init) || ((): void => {}); + const init = (lifeCycles && lifeCycles.init) || ((): void => { }); try { await init(this); @@ -198,10 +285,6 @@ export class Editor extends (EventEmitter as any) implements IEditor { } } - getPreference() { - return this.preference; - } - initHooks = (hooks: HookConfig[]) => { this.hooks = hooks.map((hook) => ({ ...hook, @@ -215,7 +298,7 @@ export class Editor extends (EventEmitter as any) implements IEditor { registerHooks = (hooks: HookConfig[]) => { this.initHooks(hooks).forEach(({ message, type, handler }) => { if (['on', 'once'].indexOf(type) !== -1) { - this[type](message, handler); + this[type]((message as any), handler); } }); }; @@ -226,17 +309,7 @@ export class Editor extends (EventEmitter as any) implements IEditor { }); }; - /* eslint-disable */ - private waits = new Map< - KeyType, - Array<{ - once?: boolean; - resolve: (data: any) => void; - }> - >(); - /* eslint-enable */ - - private notifyGot(key: KeyType) { + private notifyGot(key: IPublicTypeEditorValueKey) { let waits = this.waits.get(key); if (!waits) { return; @@ -256,7 +329,7 @@ export class Editor extends (EventEmitter as any) implements IEditor { } } - private setWait(key: KeyType, resolve: (data: any) => void, once?: boolean) { + private setWait(key: IPublicTypeEditorValueKey, resolve: (data: any) => void, once?: boolean) { const waits = this.waits.get(key); if (waits) { waits.push({ resolve, once }); @@ -265,7 +338,7 @@ export class Editor extends (EventEmitter as any) implements IEditor { } } - private delWait(key: KeyType, fn: any) { + private delWait(key: IPublicTypeEditorValueKey, fn: any) { const waits = this.waits.get(key); if (!waits) { return; @@ -281,3 +354,5 @@ export class Editor extends (EventEmitter as any) implements IEditor { } } } + +export const commonEvent = new EventBus(new EventEmitter()); diff --git a/packages/editor-core/src/event-bus.ts b/packages/editor-core/src/event-bus.ts new file mode 100644 index 0000000000..ae9d28905b --- /dev/null +++ b/packages/editor-core/src/event-bus.ts @@ -0,0 +1,109 @@ +import { IPublicApiEvent } from '@alilc/lowcode-types'; +import { Logger } from '@alilc/lowcode-utils'; +import EventEmitter from 'events'; + +const logger = new Logger({ level: 'warn', bizName: 'event-bus' }); +const moduleLogger = new Logger({ level: 'warn', bizName: 'module-event-bus' }); + +export interface IEventBus extends IPublicApiEvent { + removeListener(event: string | symbol, listener: (...args: any[]) => void): any; + addListener(event: string | symbol, listener: (...args: any[]) => void): any; + setMaxListeners(n: number): any; + removeAllListeners(event?: string | symbol): any; +} + +export class EventBus implements IEventBus { + private readonly eventEmitter: EventEmitter; + private readonly name?: string; + + /** + * 内核触发的事件名 + */ + readonly names = []; + + constructor(emitter: EventEmitter, name?: string) { + this.eventEmitter = emitter; + this.name = name; + } + + private getMsgPrefix(type: string): string { + if (this.name && this.name.length > 0) { + return `[${this.name}][event-${type}]`; + } else { + return `[*][event-${type}]`; + } + } + + private getLogger(): Logger { + if (this.name && this.name.length > 0) { + return moduleLogger; + } else { + return logger; + } + } + + /** + * 监听事件 + * @param event 事件名称 + * @param listener 事件回调 + */ + on(event: string, listener: (...args: any[]) => void): () => void { + this.eventEmitter.on(event, listener); + this.getLogger().debug(`${this.getMsgPrefix('on')} ${event}`); + return () => { + this.off(event, listener); + }; + } + + prependListener(event: string, listener: (...args: any[]) => void): () => void { + this.eventEmitter.prependListener(event, listener); + this.getLogger().debug(`${this.getMsgPrefix('prependListener')} ${event}`); + return () => { + this.off(event, listener); + }; + } + + /** + * 取消监听事件 + * @param event 事件名称 + * @param listener 事件回调 + */ + off(event: string, listener: (...args: any[]) => void) { + this.eventEmitter.off(event, listener); + this.getLogger().debug(`${this.getMsgPrefix('off')} ${event}`); + } + + /** + * 触发事件 + * @param event 事件名称 + * @param args 事件参数 + * @returns + */ + emit(event: string, ...args: any[]) { + this.eventEmitter.emit(event, ...args); + this.getLogger().debug(`${this.getMsgPrefix('emit')} name: ${event}, args: `, ...args); + } + + removeListener(event: string | symbol, listener: (...args: any[]) => void): any { + return this.eventEmitter.removeListener(event, listener); + } + + addListener(event: string | symbol, listener: (...args: any[]) => void): any { + return this.eventEmitter.addListener(event, listener); + } + + setMaxListeners(n: number): any { + return this.eventEmitter.setMaxListeners(n); + } + removeAllListeners(event?: string | symbol): any { + return this.eventEmitter.removeAllListeners(event); + } +} + +export const createModuleEventBus = (moduleName: string, maxListeners?: number): IEventBus => { + const emitter = new EventEmitter(); + if (maxListeners) { + emitter.setMaxListeners(maxListeners); + } + return new EventBus(emitter, moduleName); +}; \ No newline at end of file diff --git a/packages/editor-core/src/hotkey.ts b/packages/editor-core/src/hotkey.ts index 4375c98486..d0dd40cc24 100644 --- a/packages/editor-core/src/hotkey.ts +++ b/packages/editor-core/src/hotkey.ts @@ -1,6 +1,6 @@ import { isEqual } from 'lodash'; import { globalContext } from './di'; -import { Editor } from './editor'; +import { IPublicTypeHotkeyCallback, IPublicTypeHotkeyCallbackConfig, IPublicTypeHotkeyCallbacks, IPublicApiHotkey } from '@alilc/lowcode-types'; interface KeyMap { [key: number]: string; @@ -14,23 +14,8 @@ interface ActionEvent { type: string; } -interface HotkeyCallbacks { - [key: string]: HotkeyCallbackCfg[]; -} - interface HotkeyDirectMap { - [key: string]: HotkeyCallback; -} - -export type HotkeyCallback = (e: KeyboardEvent, combo?: string) => any | false; - -interface HotkeyCallbackCfg { - callback: HotkeyCallback; - modifiers: string[]; - action: string; - seq?: string; - level?: number; - combo?: string; + [key: string]: IPublicTypeHotkeyCallback; } interface KeyInfo { @@ -329,10 +314,11 @@ function getKeyInfo(combination: string, action?: string): KeyInfo { * if your callback function returns false this will use the jquery * convention - prevent default and stop propogation on the event */ -function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string, sequence?: string): void { +function fireCallback(callback: IPublicTypeHotkeyCallback, e: KeyboardEvent, combo?: string, sequence?: string): void { try { - const editor = globalContext.get(Editor); - const designer = editor.get('designer'); + const workspace = globalContext.get('workspace'); + const editor = workspace.isActive ? workspace.window?.editor : globalContext.get('editor'); + const designer = editor?.get('designer'); const node = designer?.currentSelection?.getNodes()?.[0]; const npm = node?.componentMeta?.npm; const selected = @@ -341,7 +327,7 @@ function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string e.preventDefault(); e.stopPropagation(); } - editor?.emit('hotkey.callback.call', { + editor?.eventBus.emit('hotkey.callback.call', { callback, e, combo, @@ -353,8 +339,12 @@ function fireCallback(callback: HotkeyCallback, e: KeyboardEvent, combo?: string } } -export class Hotkey { - private callBacks: HotkeyCallbacks = {}; +export interface IHotKey extends Omit<IPublicApiHotkey, 'bind' | 'callbacks'> { + activate(activate: boolean): void; +} + +export class Hotkey implements IHotKey { + callBacks: IPublicTypeHotkeyCallbacks = {}; private directMap: HotkeyDirectMap = {}; @@ -368,6 +358,16 @@ export class Hotkey { private nextExpectedAction: boolean | string = false; + private isActivate = true; + + constructor(readonly viewName: string = 'global') { + this.mount(window); + } + + activate(activate: boolean): void { + this.isActivate = activate; + } + mount(window: Window) { const { document } = window; const handleKeyEvent = this.handleKeyEvent.bind(this); @@ -381,12 +381,12 @@ export class Hotkey { }; } - bind(combos: string[] | string, callback: HotkeyCallback, action?: string): Hotkey { + bind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string): Hotkey { this.bindMultiple(Array.isArray(combos) ? combos : [combos], callback, action); return this; } - unbind(combos: string[] | string, callback: HotkeyCallback, action?: string) { + unbind(combos: string[] | string, callback: IPublicTypeHotkeyCallback, action?: string) { const combinations = Array.isArray(combos) ? combos : [combos]; combinations.forEach(combination => { @@ -431,10 +431,10 @@ export class Hotkey { sequenceName?: string, combination?: string, level?: number, - ): HotkeyCallbackCfg[] { + ): IPublicTypeHotkeyCallbackConfig[] { let i: number; - let callback: HotkeyCallbackCfg; - const matches: HotkeyCallbackCfg[] = []; + let callback: IPublicTypeHotkeyCallbackConfig; + const matches: IPublicTypeHotkeyCallbackConfig[] = []; const action: string = e.type; // if there are no events related to this keycode @@ -485,7 +485,7 @@ export class Hotkey { } private handleKey(character: string, modifiers: string[], e: KeyboardEvent): void { - const callbacks: HotkeyCallbackCfg[] = this.getMatches(character, modifiers, e); + const callbacks: IPublicTypeHotkeyCallbackConfig[] = this.getMatches(character, modifiers, e); let i: number; const doNotReset: SequenceLevels = {}; let maxLevel = 0; @@ -542,6 +542,9 @@ export class Hotkey { } private handleKeyEvent(e: KeyboardEvent): void { + if (!this.isActivate) { + return; + } const character = characterFromEvent(e); // no character found then stop @@ -565,7 +568,7 @@ export class Hotkey { this.resetTimer = window.setTimeout(this.resetSequences, 1000); } - private bindSequence(combo: string, keys: string[], callback: HotkeyCallback, action?: string): void { + private bindSequence(combo: string, keys: string[], callback: IPublicTypeHotkeyCallback, action?: string): void { // const self: any = this; this.sequenceLevels[combo] = 0; const increaseSequence = (nextAction: string) => { @@ -593,7 +596,7 @@ export class Hotkey { private bindSingle( combination: string, - callback: HotkeyCallback, + callback: IPublicTypeHotkeyCallback, action?: string, sequenceName?: string, level?: number, @@ -638,12 +641,9 @@ export class Hotkey { }); } - private bindMultiple(combinations: string[], callback: HotkeyCallback, action?: string) { + private bindMultiple(combinations: string[], callback: IPublicTypeHotkeyCallback, action?: string) { for (const item of combinations) { this.bindSingle(item, callback, action); } } } - -export const hotkey = new Hotkey(); -hotkey.mount(window); diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index 61cb0a517c..c4a54bccde 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -5,3 +5,5 @@ export * from './di'; export * from './hotkey'; export * from './widgets'; export * from './config'; +export * from './event-bus'; +export * from './command'; diff --git a/packages/editor-core/src/intl/global-locale.ts b/packages/editor-core/src/intl/global-locale.ts index 8742788022..fd67bb51f9 100644 --- a/packages/editor-core/src/intl/global-locale.ts +++ b/packages/editor-core/src/intl/global-locale.ts @@ -1,5 +1,8 @@ -import { EventEmitter } from 'events'; +import { IEventBus, createModuleEventBus } from '../event-bus'; import { obx, computed } from '../utils/obx'; +import { Logger } from '@alilc/lowcode-utils'; + +const logger = new Logger({ level: 'warn', bizName: 'globalLocale' }); const languageMap: { [key: string]: string } = { en: 'en-US', @@ -30,7 +33,7 @@ const languageMap: { [key: string]: string } = { const LowcodeConfigKey = 'ali-lowcode-config'; class GlobalLocale { - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('GlobalLocale'); @obx.ref private _locale?: string; @@ -41,13 +44,8 @@ class GlobalLocale { // TODO: store 1 & store 2 abstract out as custom implements - // store 1: config from window - let locale: string = getConfig('locale'); - if (locale) { - return languageMap[locale] || locale.replace('_', '-'); - } - - // store 2: config from storage + // store 1: config from storage + let result = null; if (hasLocalStorage(window)) { const store = window.localStorage; let config: any; @@ -57,28 +55,41 @@ class GlobalLocale { // ignore; } if (config?.locale) { - return (config.locale || '').replace('_', '-'); + result = (config.locale || '').replace('_', '-'); + logger.debug(`getting locale from localStorage: ${result}`); } } - - // store 2: config from system - const { navigator } = window as any; - if (navigator.language) { - const lang = (navigator.language as string); - return languageMap[lang] || lang.replace('_', '-'); - } else if (navigator.browserLanguage) { - const it = navigator.browserLanguage.split('-'); - locale = it[0]; - if (it[1]) { - locale += `-${ it[1].toUpperCase()}`; + if (!result) { + // store 2: config from window + let localeFromConfig: string = getConfig('locale'); + if (localeFromConfig) { + result = languageMap[localeFromConfig] || localeFromConfig.replace('_', '-'); + logger.debug(`getting locale from config: ${result}`); } } - if (!locale) { - locale = 'zh-CN'; + if (!result) { + // store 3: config from system + const { navigator } = window as any; + if (navigator.language) { + const lang = (navigator.language as string); + return languageMap[lang] || lang.replace('_', '-'); + } else if (navigator.browserLanguage) { + const it = navigator.browserLanguage.split('-'); + let localeFromSystem = it[0]; + if (it[1]) { + localeFromSystem += `-${it[1].toUpperCase()}`; + } + result = localeFromSystem; + logger.debug(`getting locale from system: ${result}`); + } } - - return locale; + if (!result) { + logger.warn('something when wrong when trying to get locale, use zh-CN as default, please check it out!'); + result = 'zh-CN'; + } + this._locale = result; + return result; } constructor() { @@ -86,6 +97,7 @@ class GlobalLocale { } setLocale(locale: string) { + logger.info(`setting locale to ${locale}`); if (locale === this.locale) { return; } @@ -136,12 +148,5 @@ function hasLocalStorage(obj: any): obj is WindowLocalStorage { } let globalLocale = new GlobalLocale(); -// let globalLocale: GlobalLocale; -// if ((window as any).__GlobalLocale) { -// globalLocale = (window as any).__GlobalLocale as any; -// } else { -// globalLocale = new GlobalLocale(); -// (window as any).__GlobalLocale = globalLocale; -// } export { globalLocale }; diff --git a/packages/editor-core/src/intl/index.ts b/packages/editor-core/src/intl/index.ts index 01cb452cd8..99e99a4fb9 100644 --- a/packages/editor-core/src/intl/index.ts +++ b/packages/editor-core/src/intl/index.ts @@ -1,8 +1,9 @@ import { ReactNode, Component, createElement } from 'react'; import { IntlMessageFormat } from 'intl-messageformat'; import { globalLocale } from './global-locale'; -import { isI18nData } from '@alilc/lowcode-types'; -import { observer, computed } from '../utils'; +import { isI18nData } from '@alilc/lowcode-utils'; +import { observer } from '../utils'; +import { IPublicTypeI18nData } from '@alilc/lowcode-types'; function generateTryLocales(locale: string) { const tries = [locale, locale.replace('-', '_')]; @@ -26,18 +27,9 @@ function injectVars(msg: string, params: any, locale: string): string { } const formater = new IntlMessageFormat(msg, locale); return formater.format(params as any) as string; - /* - - return template.replace(/({\w+})/g, (_, $1) => { - const key = (/\d+/.exec($1) || [])[0] as any; - if (key && params[key] != null) { - return params[key]; - } - return $1; - }); */ } -export function intl(data: any, params?: object): ReactNode { +export function intl(data: IPublicTypeI18nData | string, params?: object): ReactNode { if (!isI18nData(data)) { return data; } diff --git a/packages/editor-core/src/utils/app-preset.ts b/packages/editor-core/src/utils/app-preset.ts index 469c8d73ac..8b22575ecc 100644 --- a/packages/editor-core/src/utils/app-preset.ts +++ b/packages/editor-core/src/utils/app-preset.ts @@ -7,7 +7,7 @@ declare global { } } -// 根据url参数设置debug选项 +// 根据 url 参数设置 debug 选项 const debugRegRes = /_?debug=(.*?)(&|$)/.exec(location.search); if (debugRegRes && debugRegRes[1]) { // eslint-disable-next-line no-underscore-dangle @@ -20,14 +20,14 @@ if (debugRegRes && debugRegRes[1]) { store.remove('debug'); } -// 重要,用于矫正画布执行new Function的window对象上下文 +// 重要,用于矫正画布执行 new Function 的 window 对象上下文 // eslint-disable-next-line no-underscore-dangle window.__newFunc = (funContext: string): ((...args: any[]) => any) => { // eslint-disable-next-line no-new-func return new Function(funContext) as (...args: any[]) => any; }; -// 关闭浏览器前提醒,只有产生过交互才会生效 +// 关闭浏览器前提醒,只有产生过交互才会生效 window.onbeforeunload = function (e: Event): string { const ev = e || window.event; // 本地调试不生效 diff --git a/packages/editor-core/src/utils/assets-transform.ts b/packages/editor-core/src/utils/assets-transform.ts new file mode 100644 index 0000000000..013338751b --- /dev/null +++ b/packages/editor-core/src/utils/assets-transform.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-param-reassign */ +import { IPublicTypeAssetsJson, IPublicTypeComponentDescription, IPublicTypePackage, IPublicTypeRemoteComponentDescription } from '@alilc/lowcode-types'; + +// TODO: 该转换逻辑未来需要消化掉 +export function assetsTransform(assets: IPublicTypeAssetsJson) { + const { components, packages } = assets; + const packageMaps = (packages || []).reduce((acc: Record<string, IPublicTypePackage>, cur: IPublicTypePackage) => { + const key = cur.id || cur.package || ''; + acc[key] = cur; + return acc; + }, {} as any); + components.forEach((componentDesc: IPublicTypeComponentDescription | IPublicTypeRemoteComponentDescription) => { + let { devMode, schema, reference } = componentDesc; + if ((devMode as string) === 'lowcode') { + devMode = 'lowCode'; + } else if (devMode === 'proCode') { + devMode = 'proCode'; + } + if (devMode) { + componentDesc.devMode = devMode; + } + if (devMode === 'lowCode' && !schema && reference) { + const referenceId = reference.id || ''; + componentDesc.schema = packageMaps[referenceId].schema; + } + }); + return assets; +} \ No newline at end of file diff --git a/packages/editor-core/src/utils/focus-tracker.ts b/packages/editor-core/src/utils/focus-tracker.ts index 61ecb30427..23d509053b 100644 --- a/packages/editor-core/src/utils/focus-tracker.ts +++ b/packages/editor-core/src/utils/focus-tracker.ts @@ -1,4 +1,8 @@ export class FocusTracker { + private actives: Focusable[] = []; + + private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = []; + mount(win: Window) { const checkDown = (e: MouseEvent) => { if (this.checkModalDown(e)) { @@ -16,14 +20,10 @@ export class FocusTracker { }; } - private actives: Focusable[] = []; - get first() { return this.actives[0]; } - private modals: Array<{ checkDown: (e: MouseEvent) => boolean; checkOpen: () => boolean }> = []; - addModal(checkDown: (e: MouseEvent) => boolean, checkOpen: () => boolean) { this.modals.push({ checkDown, @@ -32,11 +32,11 @@ export class FocusTracker { } private checkModalOpen(): boolean { - return this.modals.some(item => item.checkOpen()); + return this.modals.some((item) => item.checkOpen()); } private checkModalDown(e: MouseEvent): boolean { - return this.modals.some(item => item.checkDown(e)); + return this.modals.some((item) => item.checkDown(e)); } execSave() { @@ -154,7 +154,3 @@ export class Focusable { } } } - -export const focusTracker = new FocusTracker(); - -focusTracker.mount(window); diff --git a/packages/editor-core/src/utils/logger.ts b/packages/editor-core/src/utils/logger.ts index 47ec22c6f1..212dcddab1 100644 --- a/packages/editor-core/src/utils/logger.ts +++ b/packages/editor-core/src/utils/logger.ts @@ -1,4 +1,4 @@ -import Logger, { Level } from 'zen-logger'; +import { Logger, Level } from '@alilc/lowcode-utils'; export { Logger }; diff --git a/packages/editor-core/src/utils/preference.ts b/packages/editor-core/src/utils/preference.ts index cc23092aeb..6f17a8f638 100644 --- a/packages/editor-core/src/utils/preference.ts +++ b/packages/editor-core/src/utils/preference.ts @@ -1,28 +1,27 @@ import store from 'store'; import { getLogger } from './logger'; +import { IPublicModelPreference } from '@alilc/lowcode-types'; -const logger = getLogger({ level: 'log', bizName: 'Preference' }); +const logger = getLogger({ level: 'warn', bizName: 'Preference' }); const STORAGE_KEY_PREFIX = 'ale'; /** * used to store user preferences, such as pinned status of a pannel. * save to local storage. - * - * @class PreferenceStore */ -export default class Preference { +export default class Preference implements IPublicModelPreference { getStorageKey(key: string, module?: string): string { const moduleKey = module || '__inner__'; return `${STORAGE_KEY_PREFIX}_${moduleKey}.${key}`; } - set(key: string, value: any, module?: string) { + set(key: string, value: any, module?: string): void { if (!key || typeof key !== 'string' || key.length === 0) { logger.error('Invalid key when setting preference', key); return; } const storageKey = this.getStorageKey(key, module); - logger.log('storageKey:', storageKey, 'set with value:', value); + logger.debug('storageKey:', storageKey, 'set with value:', value); store.set(storageKey, value); } @@ -33,16 +32,15 @@ export default class Preference { } const storageKey = this.getStorageKey(key, module); const result = store.get(storageKey); - logger.log('storageKey:', storageKey, 'get with result:', result); + logger.debug('storageKey:', storageKey, 'get with result:', result); return result; } + /** * check if local storage contain certain key * * @param {string} key * @param {string} module - * @returns {boolean} - * @memberof Preference */ contains(key: string, module: string): boolean { if (!key || typeof key !== 'string' || key.length === 0) { @@ -54,5 +52,4 @@ export default class Preference { return !(result === undefined || result === null); } - } \ No newline at end of file diff --git a/packages/editor-core/src/widgets/tip/help-tips.tsx b/packages/editor-core/src/widgets/tip/help-tips.tsx new file mode 100644 index 0000000000..ab5f65050e --- /dev/null +++ b/packages/editor-core/src/widgets/tip/help-tips.tsx @@ -0,0 +1,40 @@ +import { IPublicTypeHelpTipConfig, IPublicTypeTipConfig } from '@alilc/lowcode-types'; +import { Tip } from './tip'; +import { Icon } from '@alifd/next'; +import { IconProps } from '@alifd/next/types/icon'; + +export function HelpTip({ + help, + direction = 'top', + size = 'small', +}: { + help: IPublicTypeHelpTipConfig; + direction?: IPublicTypeTipConfig['direction']; + size?: IconProps['size']; +}) { + if (typeof help === 'string') { + return ( + <div> + <Icon type="help" size={size} className="lc-help-tip" /> + <Tip direction={direction}>{help}</Tip> + </div> + ); + } + + if (typeof help === 'object' && help.url) { + return ( + <div> + <a href={help.url} target="_blank" rel="noopener noreferrer"> + <Icon type="help" size={size} className="lc-help-tip" /> + </a> + <Tip direction={direction}>{help.content}</Tip> + </div> + ); + } + return ( + <div> + <Icon type="help" size="small" className="lc-help-tip" /> + <Tip direction={direction}>{help.content}</Tip> + </div> + ); +} \ No newline at end of file diff --git a/packages/editor-core/src/widgets/tip/index.ts b/packages/editor-core/src/widgets/tip/index.ts index dba4412c7f..d2b3768003 100644 --- a/packages/editor-core/src/widgets/tip/index.ts +++ b/packages/editor-core/src/widgets/tip/index.ts @@ -2,3 +2,4 @@ import './style.less'; export * from './tip'; export * from './tip-container'; +export * from './help-tips'; diff --git a/packages/editor-core/src/widgets/tip/style.less b/packages/editor-core/src/widgets/tip/style.less index 3c02e5313e..602886d060 100644 --- a/packages/editor-core/src/widgets/tip/style.less +++ b/packages/editor-core/src/widgets/tip/style.less @@ -147,7 +147,7 @@ z-index: 2; position: fixed; box-sizing: border-box; - background: rgba(0, 0, 0, 0.7); + background: var(--color-layer-tooltip-background); max-height: 400px; color: var(--color-text-reverse, rgba(255, 255, 255, 0.8)); left: 0; @@ -156,7 +156,7 @@ opacity: 0; border-radius: 3px; padding: 6px 8px; - text-shadow: 0 -1px rgba(0, 0, 0, 0.3); + text-shadow: 0 -1px var(--color-field-label, rgba(0, 0, 0, 0.3)); font-size: var(--font-size-text); line-height: 14px; max-width: 200px; @@ -178,19 +178,19 @@ height: 8px; &:after { border: 6px solid transparent; - border-top-color: rgba(0, 0, 0, 0.7); + border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7)); } } &.lc-theme-black { - background: rgba(0, 0, 0, 0.7); + background: var(--color-icon-pane, rgba(0, 0, 0, 0.7)); .lc-arrow:after { - border-top-color: rgba(0, 0, 0, 0.7); + border-top-color: var(--color-layer-tooltip-background, rgba(0, 0, 0, 0.7)); } } &.lc-theme-green { - background: #57a672; + background: var(--color-success-dark, var(--color-function-success-dark, #57a672)); .lc-arrow:after { - border-top-color: #57a672; + border-top-color: var(--color-success-dark, var(--color-function-success-dark, #57a672)); } } &.lc-visible { diff --git a/packages/editor-core/src/widgets/tip/tip-container.tsx b/packages/editor-core/src/widgets/tip/tip-container.tsx index 81750bd6a4..ed3af589a1 100644 --- a/packages/editor-core/src/widgets/tip/tip-container.tsx +++ b/packages/editor-core/src/widgets/tip/tip-container.tsx @@ -1,14 +1,13 @@ import { Component } from 'react'; +import ReactDOM from 'react-dom'; import { TipItem } from './tip-item'; import { tipHandler } from './tip-handler'; export class TipContainer extends Component { + private dispose?: () => void; shouldComponentUpdate() { return false; } - - private dispose?: () => void; - componentDidMount() { const over = (e: MouseEvent) => tipHandler.setTarget(e.target as any); const down = () => tipHandler.hideImmediately(); @@ -27,10 +26,11 @@ export class TipContainer extends Component { } render() { - return ( + return ReactDOM.createPortal( <div className="lc-tips-container"> <TipItem /> - </div> + </div>, + document.querySelector('body')!, ); } } diff --git a/packages/editor-core/src/widgets/tip/tip-handler.ts b/packages/editor-core/src/widgets/tip/tip-handler.ts index e6e6ef6ee7..63193ea854 100644 --- a/packages/editor-core/src/widgets/tip/tip-handler.ts +++ b/packages/editor-core/src/widgets/tip/tip-handler.ts @@ -1,7 +1,7 @@ -import { EventEmitter } from 'events'; -import { TipConfig } from '@alilc/lowcode-types'; +import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; +import { IEventBus, createModuleEventBus } from '../../event-bus'; -export interface TipOptions extends TipConfig { +export interface TipOptions extends IPublicTypeTipConfig { target: HTMLElement; } @@ -12,7 +12,7 @@ class TipHandler { private hideDelay: number | null = null; - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('TipHandler'); setTarget(target: HTMLElement) { const tip = findTip(target); @@ -132,8 +132,8 @@ function findTip(target: HTMLElement | null): TipOptions | null { return null; } -const tipsMap = new Map<string, TipConfig>(); -export function postTip(id: string, props: TipConfig | null) { +const tipsMap = new Map<string, IPublicTypeTipConfig>(); +export function postTip(id: string, props: IPublicTypeTipConfig | null) { if (props) { tipsMap.set(id, props); } else { diff --git a/packages/editor-core/src/widgets/tip/tip-item.tsx b/packages/editor-core/src/widgets/tip/tip-item.tsx index 63386fcd18..c56d747ffa 100644 --- a/packages/editor-core/src/widgets/tip/tip-item.tsx +++ b/packages/editor-core/src/widgets/tip/tip-item.tsx @@ -1,6 +1,6 @@ import { Component } from 'react'; import classNames from 'classnames'; -import { TipConfig } from '@alilc/lowcode-types'; +import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; import { intl } from '../../intl'; import { resolvePosition } from './utils'; import { tipHandler } from './tip-handler'; @@ -105,7 +105,7 @@ export class TipItem extends Component { } render() { - const tip: TipConfig = tipHandler.tip || ({} as any); + const tip: IPublicTypeTipConfig = tipHandler.tip || ({} as any); const className = classNames('lc-tip', tip.className, tip && tip.theme ? `lc-theme-${tip.theme}` : null); this.originClassName = className; @@ -113,7 +113,7 @@ export class TipItem extends Component { return ( <div className={className} - ref={ref => { + ref={(ref) => { this.shell = ref; }} > diff --git a/packages/editor-core/src/widgets/tip/tip.tsx b/packages/editor-core/src/widgets/tip/tip.tsx index 9c92d1eed7..92a683baa5 100644 --- a/packages/editor-core/src/widgets/tip/tip.tsx +++ b/packages/editor-core/src/widgets/tip/tip.tsx @@ -1,9 +1,9 @@ import { Component } from 'react'; -import { TipConfig } from '@alilc/lowcode-types'; +import { IPublicTypeTipConfig } from '@alilc/lowcode-types'; import { uniqueId } from '@alilc/lowcode-utils'; import { postTip } from './tip-handler'; -export class Tip extends Component<TipConfig> { +export class Tip extends Component<IPublicTypeTipConfig> { private id = uniqueId('tips$'); componentWillUnmount() { diff --git a/packages/editor-core/src/widgets/title/index.tsx b/packages/editor-core/src/widgets/title/index.tsx index bacfee9f16..7df2676f92 100644 --- a/packages/editor-core/src/widgets/title/index.tsx +++ b/packages/editor-core/src/widgets/title/index.tsx @@ -1,7 +1,7 @@ import { Component, isValidElement, ReactNode } from 'react'; import classNames from 'classnames'; -import { createIcon } from '@alilc/lowcode-utils'; -import { TitleContent, isI18nData, I18nData } from '@alilc/lowcode-types'; +import { createIcon, isI18nData, isTitleConfig } from '@alilc/lowcode-utils'; +import { IPublicTypeI18nData, IPublicTypeTitleConfig, IPublicTypeTitleProps } from '@alilc/lowcode-types'; import { intl } from '../../intl'; import { Tip } from '../tip'; import './title.less'; @@ -36,13 +36,7 @@ import './title.less'; return fragments; } -export class Title extends Component<{ - title: TitleContent; - className?: string; - onClick?: () => void; - match?: boolean; - keywords?: string; -}> { +export class Title extends Component<IPublicTypeTitleProps> { constructor(props: any) { super(props); this.handleClick = this.handleClick.bind(this); @@ -60,7 +54,7 @@ export class Title extends Component<{ onClick && onClick(e); } - renderLabel = (label: string | I18nData | ReactNode) => { + renderLabel = (label: string | IPublicTypeI18nData | ReactNode) => { let { match, keywords } = this.props; if (!label) { @@ -88,7 +82,8 @@ export class Title extends Component<{ render() { // eslint-disable-next-line prefer-const - let { title, className } = this.props; + const { title, className } = this.props; + let _title: IPublicTypeTitleConfig; if (title == null) { return null; } @@ -96,34 +91,40 @@ export class Title extends Component<{ return title; } if (typeof title === 'string' || isI18nData(title)) { - title = { label: title }; + _title = { label: title }; + } else if (isTitleConfig(title)) { + _title = title; + } else { + _title = { + label: title, + }; } - const icon = title.icon ? createIcon(title.icon, { size: 20 }) : null; + const icon = _title.icon ? createIcon(_title.icon, { size: 20 }) : null; let tip: any = null; - if (title.tip) { - if (isValidElement(title.tip) && title.tip.type === Tip) { - tip = title.tip; + if (_title.tip) { + if (isValidElement(_title.tip) && _title.tip.type === Tip) { + tip = _title.tip; } else { const tipProps = - typeof title.tip === 'object' && !(isValidElement(title.tip) || isI18nData(title.tip)) - ? title.tip - : { children: title.tip }; + typeof _title.tip === 'object' && !(isValidElement(_title.tip) || isI18nData(_title.tip)) + ? _title.tip + : { children: _title.tip }; tip = <Tip {...tipProps} />; } } return ( <span - className={classNames('lc-title', className, title.className, { + className={classNames('lc-title', className, _title.className, { 'has-tip': !!tip, - 'only-icon': !title.label, + 'only-icon': !_title.label, })} onClick={this.handleClick} > {icon ? <b className="lc-title-icon">{icon}</b> : null} - {this.renderLabel(title.label)} + {this.renderLabel(_title.label)} {tip} </span> ); diff --git a/packages/editor-core/src/widgets/title/title.less b/packages/editor-core/src/widgets/title/title.less index e9d0765522..f6747ef474 100644 --- a/packages/editor-core/src/widgets/title/title.less +++ b/packages/editor-core/src/widgets/title/title.less @@ -21,7 +21,7 @@ cursor: help; text-decoration-line: underline; text-decoration-style: dashed; - text-decoration-color: rgba(31, 56, 88, .3); + text-decoration-color: var(--color-text-light, rgba(31, 56, 88, .3)); } line-height: initial !important; word-break: break-all; 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/build.json b/packages/editor-skeleton/build.json index 77627cdf98..d0aec10385 100644 --- a/packages/editor-skeleton/build.json +++ b/packages/editor-skeleton/build.json @@ -1,9 +1,9 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "build-plugin-fusion", ["build-plugin-moment-locales", { "locales": ["zh-cn"] }] ] -} \ No newline at end of file +} diff --git a/packages/editor-skeleton/build.test.json b/packages/editor-skeleton/build.test.json new file mode 100644 index 0000000000..10d18109b8 --- /dev/null +++ b/packages/editor-skeleton/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-skeleton/jest.config.js b/packages/editor-skeleton/jest.config.js new file mode 100644 index 0000000000..8a9b2000ca --- /dev/null +++ b/packages/editor-skeleton/jest.config.js @@ -0,0 +1,29 @@ +const fs = require('fs'); +const { join } = require('path'); +const esModules = [].join('|'); +const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); + +const jestConfig = { + // transform: { + // '^.+\\.[jt]sx?$': 'babel-jest', + // // '^.+\\.(ts|tsx)$': 'ts-jest', + // // '^.+\\.(js|jsx)$': 'babel-jest', + // }, + transformIgnorePatterns: [ + `/node_modules/(?!${esModules})/`, + ], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!**/node_modules/**', + '!**/vendor/**', + ], +}; + +// 只对本仓库内的 pkg 做 mapping +jestConfig.moduleNameMapper = {}; +jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '<rootDir>/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/editor-skeleton/package.json b/packages/editor-skeleton/package.json index a4bce4a42c..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.0.15", + "version": "1.3.2", "description": "alibaba lowcode editor skeleton", "main": "lib/index.js", "module": "es/index.js", @@ -10,7 +10,8 @@ "es" ], "scripts": { - "build": "build-scripts build --skip-demo" + "test": "build-scripts test --config build.test.json", + "build": "build-scripts build" }, "keywords": [ "lowcode", @@ -18,10 +19,10 @@ ], "dependencies": { "@alifd/next": "^1.20.12", - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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" @@ -30,7 +31,6 @@ "@alib/build-scripts": "^0.1.3", "@types/react": "^16.9.13", "@types/react-dom": "^16.9.4", - "build-plugin-component": "^0.2.7", "build-plugin-fusion": "^0.1.0", "build-plugin-moment-locales": "^0.1.0" }, @@ -42,5 +42,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/editor-skeleton" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/editor-skeleton/src/area.ts b/packages/editor-skeleton/src/area.ts index 94f4c36144..8dc711084b 100644 --- a/packages/editor-skeleton/src/area.ts +++ b/packages/editor-skeleton/src/area.ts @@ -1,10 +1,22 @@ +/* eslint-disable max-len */ import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core'; -import WidgetContainer from './widget/widget-container'; -import { Skeleton } from './skeleton'; +import { Logger } from '@alilc/lowcode-utils'; +import { IPublicTypeWidgetBaseConfig } from '@alilc/lowcode-types'; +import { WidgetContainer } from './widget/widget-container'; +import { ISkeleton } from './skeleton'; import { IWidget } from './widget/widget'; -import { IWidgetBaseConfig } from './types'; -export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = IWidget> { +const logger = new Logger({ level: 'warn', bizName: 'skeleton:area' }); +export interface IArea<C, T> { + isEmpty(): boolean; + add(config: T | C): T; + remove(config: T | string): number; + setVisible(flag: boolean): void; + hide(): void; + show(): void; +} + +export class Area<C extends IPublicTypeWidgetBaseConfig = any, T extends IWidget = IWidget> implements IArea<C, T> { @obx private _visible = true; @computed get visible() { @@ -23,7 +35,9 @@ export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = readonly container: WidgetContainer<T, C>; - constructor(readonly skeleton: Skeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent = false) { + private lastCurrent: T | null = null; + + constructor(readonly skeleton: ISkeleton, readonly name: string, handle: (item: T | C) => T, private exclusive?: boolean, defaultSetCurrent = false) { makeObservable(this); this.container = skeleton.createContainer(name, handle, exclusive, () => this.visible, defaultSetCurrent); } @@ -35,6 +49,7 @@ export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = add(config: T | C): T { const item = this.container.get(config.name); if (item) { + logger.warn(`The ${config.name} has already been added to skeleton.`); return item; } return this.container.add(config); @@ -44,8 +59,6 @@ export default class Area<C extends IWidgetBaseConfig = any, T extends IWidget = return this.container.remove(config); } - private lastCurrent: T | null = null; - setVisible(flag: boolean) { if (this.exclusive) { const { current } = this.container; diff --git a/packages/editor-skeleton/src/components/field/fields.tsx b/packages/editor-skeleton/src/components/field/fields.tsx index 3ac4491dda..21ae93eb25 100644 --- a/packages/editor-skeleton/src/components/field/fields.tsx +++ b/packages/editor-skeleton/src/components/field/fields.tsx @@ -1,18 +1,23 @@ -import { Component, MouseEvent } from 'react'; +/* eslint-disable react/no-unused-prop-types */ +import { Component, ErrorInfo, MouseEvent } from 'react'; import { isObject } from 'lodash'; import classNames from 'classnames'; import { Icon } from '@alifd/next'; import { Title } from '@alilc/lowcode-editor-core'; -import { IEditor, TitleContent } from '@alilc/lowcode-types'; +import { IPublicModelEditor, IPublicTypeTitleContent } from '@alilc/lowcode-types'; import { PopupPipe, PopupContext } from '../popup'; import './index.less'; import InlineTip from './inlinetip'; +import { intl } from '../../locale'; +import { Logger } from '@alilc/lowcode-utils'; + +const logger = new Logger({ level: 'warn', bizName: 'skeleton:field' }); export interface FieldProps { className?: string; meta?: { package: string; componentName: string } | string; - title?: TitleContent | null; - editor?: IEditor; + title?: IPublicTypeTitleContent | null; + editor?: IPublicModelEditor; defaultDisplay?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; collapsed?: boolean; valueState?: number; @@ -29,6 +34,10 @@ export class Field extends Component<FieldProps> { hasError: false, }; + private body: HTMLDivElement | null = null; + + private dispose?: () => void; + constructor(props: any) { super(props); this.handleClear = this.handleClear.bind(this); @@ -45,10 +54,6 @@ export class Field extends Component<FieldProps> { onExpandChange && onExpandChange(!collapsed); }; - private body: HTMLDivElement | null = null; - - private dispose?: () => void; - private deployBlockTesting() { if (this.dispose) { this.dispose(); @@ -99,28 +104,34 @@ export class Field extends Component<FieldProps> { } static getDerivedStateFromError() { - return { hasError: true }; + return { + hasError: true, + }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + logger.error(`${this.props.title} has error`, error, errorInfo); } getTipContent(propName: string, tip?: any): any { let tipContent = ( <div> - <div>属性:{propName}</div> + <div>{intl('Attribute: ')}{propName}</div> </div> ); if (isObject(tip)) { tipContent = ( <div> - <div>属性:{propName}</div> - <div>说明:{(tip as any).content}</div> + <div>{intl('Attribute: ')}{propName}</div> + <div>{intl('Description: ')}{(tip as any).content}</div> </div> ); } else if (tip) { tipContent = ( <div> - <div>属性:{propName}</div> - <div>说明:{tip}</div> + <div>{intl('Attribute: ')}{propName}</div> + <div>{intl('Description: ')}{tip}</div> </div> ); } @@ -129,7 +140,7 @@ export class Field extends Component<FieldProps> { clickHandler(event?: MouseEvent) { const { editor, name, title, meta } = this.props; - editor?.emit('setting.setter.field.click', { name, title, meta, event }); + editor?.eventBus.emit('setting.setter.field.click', { name, title, meta, event }); } render() { @@ -192,39 +203,6 @@ export class Field extends Component<FieldProps> { */ function createValueState(/* valueState?: number, onClear?: (e: React.MouseEvent) => void */) { return null; - /* - let tip: any = null; - let className = 'lc-valuestate'; - let icon: any = null; - if (valueState) { - if (valueState < 0) { - // multiple value 橘黄色点: tip:多种值,点击清除 - tip = intlNode('Multiple Value, Click to Clear'); - className += ' valuestate-multiple'; - icon = <IconClear size={6} />; - } else if (valueState === 10) { - // isset orangered tip: 必填项 - tip = intlNode('Required'); - className += ' valuestate-required'; - onClear = undefined; - } else if (valueState > 0) { - // isset 蓝点 tip: 已设置值,点击清除 - tip = intlNode('Setted Value, Click to Clear'); - className += ' valuestate-isset'; - icon = <IconClear size={6} />; - } - } else { - onClear = undefined; - // unset 占位空间 - } - - return ( - <i className={className} onClick={onClear}> - {icon} - {tip && <Tip>{tip}</Tip>} - </i> - ); - */ } export interface PopupFieldProps extends FieldProps { diff --git a/packages/editor-skeleton/src/components/field/index.less b/packages/editor-skeleton/src/components/field/index.less index 249abef570..8c1facfbaf 100644 --- a/packages/editor-skeleton/src/components/field/index.less +++ b/packages/editor-skeleton/src/components/field/index.less @@ -14,54 +14,6 @@ .lc-field-title { display: flex; align-items: center; - .lc-valuestate { - height: 6px; - width: 6px; - min-width: 6px; - border-radius: 100%; - margin-right: 2px; - pointer-events: none; - display: inline-flex; - align-items: center; - justify-content: center; - color: white; - > svg { - display: none; - } - &.valuestate-multiple { - background-color: rgb(232, 145, 83); - pointer-events: auto; - &:hover { - background-color: rgb(223, 139, 30); - cursor: pointer; - transform: scale(2); - transform-origin: center; - > svg { - display: block; - } - } - } - &.valuestate-isset { - background-color: rgba(124, 177, 238, 0.6); - pointer-events: auto; - &:hover { - background-color: rgb(45, 126, 219); - cursor: pointer; - transform: scale(2); - transform-origin: center; - > svg { - display: block; - } - } - } - &.valuestate-required { - background-color: rgb(250, 82, 76); - pointer-events: auto; - &:hover { - background-color: rgb(224, 46, 40); - } - } - } } .lc-field-icon { transform-origin: center; @@ -92,11 +44,7 @@ &.lc-block-field, &.lc-accordion-field, &.lc-entry-field { display: block; - &:first-child { - > .lc-field-head { - // border-top: none; - } - } + > .lc-field-head { height: 32px; display: flex; @@ -110,7 +58,7 @@ user-select: none; > .lc-field-icon { - color: #8f9bb3; + color: var(--color-icon-normal, #8f9bb3); } } @@ -128,10 +76,6 @@ } } } - - // + .lc-inline-field { - // border-top: 1px solid var(--color-line-normal); - // } } &.lc-entry-field { @@ -163,15 +107,11 @@ > .lc-field-head { cursor: pointer; } - + &.lc-field-is-collapsed { margin-bottom: 6px; } - // collapsed - // &:last-child.lc-field-is-collapsed{ - // border-bottom: 1px solid var(--color-line-normal); - // } &.lc-field-is-collapsed { > .lc-field-head .lc-field-icon { transform: rotate(180deg); @@ -180,56 +120,5 @@ display: none; } } - - // 邻近的保持上下距离 - + .lc-field { - // margin-top: @y-gap; - } - } - - // 2rd level reset - .lc-field-body { - // .lc-inline-field { - // &:first-child { - // padding-top: 0; - // } - // + .lc-accordion-field, +.lc-block-field { - // margin-top: @y-gap; - // } - // } - - // .lc-field { - // border-top: none !important; - // } - - // .lc-accordion-field, .lc-block-field { - // > .lc-field-head { - // padding-left: @x-gap; - // background: var(--color-block-background-light); - // border-bottom: none; - // border-top: none; - // > .lc-field-icon { - // // margin-right: @x-gap/2; - // margin-right: 0; - // } - // } - - // > .lc-field-body { - // padding: 8px; - // } - // } - - // 3rd level field title width should short - // .lc-field-body .lc-inline-field { - // > .lc-field-head { - // width: 50px; - // .lc-title-label { - // width: 50px; - // } - // } - // } - // >.lc-block-setter { - // flex: 1; - // } } } diff --git a/packages/editor-skeleton/src/components/field/index.ts b/packages/editor-skeleton/src/components/field/index.ts index 152de489c6..defe151cf7 100644 --- a/packages/editor-skeleton/src/components/field/index.ts +++ b/packages/editor-skeleton/src/components/field/index.ts @@ -1,11 +1,11 @@ import { ReactNode, createElement } from 'react'; -import { TitleContent } from '@alilc/lowcode-types'; +import { IPublicTypeTitleContent } from '@alilc/lowcode-types'; import './index.less'; import { Field, PopupField, EntryField, PlainField } from './fields'; export interface FieldProps { className?: string; - title?: TitleContent | null; + title?: IPublicTypeTitleContent | null; display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; collapsed?: boolean; valueState?: number; @@ -14,7 +14,7 @@ export interface FieldProps { [extra: string]: any; } -export function createField(props: FieldProps, children: ReactNode, type?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry') { +export function createField(props: FieldProps, children: ReactNode, type?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'): ReactNode { if (type === 'popup') { return createElement(PopupField, props, children); } diff --git a/packages/editor-skeleton/src/components/popup/index.tsx b/packages/editor-skeleton/src/components/popup/index.tsx index 194174d4eb..1367723a16 100644 --- a/packages/editor-skeleton/src/components/popup/index.tsx +++ b/packages/editor-skeleton/src/components/popup/index.tsx @@ -1,17 +1,33 @@ import { createContext, ReactNode, Component, PureComponent } from 'react'; -import { EventEmitter } from 'events'; import { Drawer, ConfigProvider } from '@alifd/next'; import { uniqueId } from '@alilc/lowcode-utils'; +import { IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; import './style.less'; +export interface PopupExtProps { + width?: number; + hasMask?: boolean; + trigger?: ReactNode; + canCloseByOutSideClick?: boolean + className?: string; + safeNode?: string[]; +} + +interface PopupProps extends PopupExtProps{ + content?: ReactNode, + title?: ReactNode, + actionKey?: string +} + + export const PopupContext = createContext<PopupPipe>({} as any); export class PopupPipe { - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('PopupPipe'); private currentId?: string; - create(props?: object): { + create(props?: PopupExtProps): { send: (content: ReactNode, title: ReactNode) => void; show: (target: Element) => void; } { @@ -45,13 +61,13 @@ export class PopupPipe { }; } - private popup(props: object, target?: Element) { + private popup(props: PopupProps, target?: Element) { Promise.resolve().then(() => { this.emitter.emit('popupchange', props, target); }); } - onPopupChange(fn: (props: object, target?: Element) => void): () => void { + onPopupChange(fn: (props: PopupProps, target?: Element) => void): () => void { this.emitter.on('popupchange', fn); return () => { this.emitter.removeListener('popupchange', fn); @@ -86,18 +102,23 @@ export default class PopupService extends Component<{ } } +interface StateType extends PopupProps { + visible?: boolean, + offsetX?: number, + pos?: {top: number, height: number} +} export class PopupContent extends PureComponent<{ safeId?: string; popupContainer?: string }> { static contextType = PopupContext; popupContainerId = uniqueId('popupContainer'); - state: any = { + state: StateType = { visible: false, offsetX: -300, }; private dispose = (this.context as PopupPipe).onPopupChange((props, target) => { - const state: any = { + const state: StateType = { ...props, visible: true, }; @@ -132,7 +153,7 @@ export class PopupContent extends PureComponent<{ safeId?: string; popupContaine }; render() { - const { content, visible, title, actionKey, pos, offsetX } = this.state; + const { content, visible, title, actionKey, pos, offsetX, width = 360, hasMask = false, canCloseByOutSideClick = true, safeNode = [] } = this.state; if (!visible) { return null; } @@ -146,10 +167,10 @@ export class PopupContent extends PureComponent<{ safeId?: string; popupContaine return ( <Drawer - width={360} + width={width} visible={visible} offset={[offsetX, 0]} - hasMask={false} + hasMask={hasMask} onVisibleChange={(_visible, type) => { if (avoidLaterHidden) { return; @@ -160,11 +181,11 @@ export class PopupContent extends PureComponent<{ safeId?: string; popupContaine }} trigger={<div className="lc-popup-placeholder" style={pos} />} triggerType="click" - canCloseByOutSideClick + canCloseByOutSideClick={canCloseByOutSideClick} animation={false} onClose={this.onClose} id={this.props.safeId} - safeNode={id} + safeNode={[id, ...safeNode]} closeable container={this.props.popupContainer} > diff --git a/packages/editor-skeleton/src/components/settings/main.ts b/packages/editor-skeleton/src/components/settings/main.ts index 1a1aca43c4..6cc672c907 100644 --- a/packages/editor-skeleton/src/components/settings/main.ts +++ b/packages/editor-skeleton/src/components/settings/main.ts @@ -1,6 +1,5 @@ -import { EventEmitter } from 'events'; import { Node, Designer, Selection, SettingTopEntry } from '@alilc/lowcode-designer'; -import { Editor, obx, computed, makeObservable, action } from '@alilc/lowcode-editor-core'; +import { Editor, obx, computed, makeObservable, action, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; function generateSessionId(nodes: Node[]) { return nodes @@ -10,7 +9,7 @@ function generateSessionId(nodes: Node[]) { } export class SettingsMain { - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('SettingsMain'); private _sessionId = ''; @@ -24,7 +23,7 @@ export class SettingsMain { return this._settings?.componentMeta; } - get settings() { + @computed get settings() { return this._settings; } @@ -45,11 +44,11 @@ export class SettingsMain { this.setup([]); } }; - this.editor.on('designer.selection.change', setupSelection); + this.editor.eventBus.on('designer.selection.change', setupSelection); this.disposeListener = () => { this.editor.removeListener('designer.selection.change', setupSelection); }; - const designer = await this.editor.onceGot(Designer); + const designer = await this.editor.onceGot('designer'); this.designer = designer; setupSelection(designer.currentSelection); } diff --git a/packages/editor-skeleton/src/components/settings/settings-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-pane.tsx index 1431358d1b..1561bf8bbe 100644 --- a/packages/editor-skeleton/src/components/settings/settings-pane.tsx +++ b/packages/editor-skeleton/src/components/settings/settings-pane.tsx @@ -1,16 +1,15 @@ -import { Component, MouseEvent, Fragment } from 'react'; -import { shallowIntl, createSetterContent, observer, obx, engineConfig, runInAction, globalContext } from '@alilc/lowcode-editor-core'; -import { createContent } from '@alilc/lowcode-utils'; -import { Skeleton } from '@alilc/lowcode-editor-skeleton'; -import { isSetterConfig, CustomView, isJSSlot } from '@alilc/lowcode-types'; -import { SettingField, isSettingField, SettingTopEntry, SettingEntry, ComponentMeta } from '@alilc/lowcode-designer'; +import { Component, MouseEvent, Fragment, ReactNode } from 'react'; +import { shallowIntl, observer, obx, engineConfig, runInAction } from '@alilc/lowcode-editor-core'; +import { createContent, isJSSlot, isSetterConfig, shouldUseVariableSetter } from '@alilc/lowcode-utils'; +import { Skeleton, Stage } from '@alilc/lowcode-editor-skeleton'; +import { IPublicApiSetters, IPublicTypeCustomView, IPublicTypeDynamicProps } from '@alilc/lowcode-types'; +import { ISettingEntry, IComponentMeta, ISettingField, isSettingField, ISettingTopEntry } from '@alilc/lowcode-designer'; import { createField } from '../field'; import PopupService, { PopupPipe } from '../popup'; import { SkeletonContext } from '../../context'; -// import { Icon } from '@alifd/next'; import { intl } from '../../locale'; -function isStandardComponent(componentMeta: ComponentMeta | null) { +function isStandardComponent(componentMeta: IComponentMeta | null) { if (!componentMeta) return false; const { prototype } = componentMeta; return prototype == null; @@ -31,14 +30,17 @@ function isInitialValueNotEmpty(initialValue: any) { return (initialValue !== undefined && initialValue !== null); } -type SettingFieldViewProps = { field: SettingField }; +type SettingFieldViewProps = { field: ISettingField }; type SettingFieldViewState = { fromOnChange: boolean; value: any }; + @observer class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldViewState> { static contextType = SkeletonContext; stageName: string | undefined; + setters?: IPublicApiSetters; + constructor(props: SettingFieldViewProps) { super(props); @@ -46,15 +48,17 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView const { extraProps } = field; const { display } = extraProps; - const editor = globalContext.get('editor'); - const { stages } = editor.get('skeleton') as Skeleton; + const editor = field.designer?.editor; + const skeleton = editor?.get('skeleton') as Skeleton; + const { stages } = skeleton || {}; + this.setters = editor?.get('setters'); let stageName; if (display === 'entry') { runInAction(() => { - stageName = `${field.getNode().id}_${field.name.toString()}`; + stageName = `${field.getNode().id}_${field.name?.toString()}`; // 清除原 stage,不然 content 引用的一直是老的 field,导致数据无法得到更新 stages.container.remove(stageName); - const stage = stages.add({ + stages.add({ type: 'Widget', name: stageName, content: <Fragment>{field.items.map((item, index) => createSettingFieldView(item, field, index))}</Fragment>, @@ -67,20 +71,49 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView this.stageName = stageName; } - render() { - const { field } = this.props; - const { extraProps, componentMeta } = field; - const { condition, defaultValue } = extraProps; - let visible; + get field() { + return this.props.field; + } + + get visible() { + const { extraProps } = this.field; + const { condition } = extraProps; try { - visible = typeof condition === 'function' ? condition(field.internalToShellPropEntry()) !== false : true; + return typeof condition === 'function' ? condition(this.field.internalToShellField()) !== false : true; } catch (error) { console.error('exception when condition (hidden) is excuted', error); } - const { setter } = field; + return true; + } + + get ignoreDefaultValue(): boolean { + const { extraProps } = this.field; + const { ignoreDefaultValue } = extraProps; + try { + if (typeof ignoreDefaultValue === 'function') { + return ignoreDefaultValue(this.field.internalToShellField()); + } + return false; + } catch (error) { + console.error('exception when ignoreDefaultValue is excuted', error); + } - let setterProps: any = {}; + return false; + } + + get setterInfo(): { + setterProps: any; + initialValue: any; + setterType: any; + } { + const { extraProps, componentMeta } = this.field; + const { defaultValue } = extraProps; + + const { setter } = this.field; + let setterProps: { + setters?: (ReactNode | string)[]; + } & Record<string, unknown> | IPublicTypeDynamicProps = {}; let setterType: any; let initialValue: any = null; @@ -94,7 +127,7 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView if (setter.props) { setterProps = setter.props; if (typeof setterProps === 'function') { - setterProps = setterProps(field.internalToShellPropEntry()); + setterProps = setterProps(this.field.internalToShellField()); } } if (setter.initialValue != null) { @@ -104,56 +137,95 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView setterType = setter; } - let value = null; if (defaultValue != null && !('defaultValue' in setterProps)) { setterProps.defaultValue = defaultValue; if (initialValue == null) { initialValue = defaultValue; } } - if (field.valueState === -1) { + + if (this.field.valueState === -1) { setterProps.multiValue = true; if (!('placeholder' in setterProps)) { setterProps.placeholder = intl('Multiple Value'); } + } + + // 根据是否支持变量配置做相应的更改 + const supportVariable = this.field.extraProps?.supportVariable; + // supportVariableGlobally 只对标准组件生效,vc 需要单独配置 + const supportVariableGlobally = engineConfig.get('supportVariableGlobally', false) && isStandardComponent(componentMeta); + const isUseVariableSetter = shouldUseVariableSetter(supportVariable, supportVariableGlobally); + if (isUseVariableSetter === false) { + return { + setterProps, + initialValue, + setterType, + }; + } + + if (setterType === 'MixedSetter') { + // VariableSetter 不单独使用 + if (Array.isArray(setterProps.setters) && !setterProps.setters.includes('VariableSetter')) { + setterProps.setters.push('VariableSetter'); + } } else { - value = field.getValue(); + setterType = 'MixedSetter'; + setterProps = { + setters: [ + setter, + 'VariableSetter', + ], + }; } + return { + setterProps, + initialValue, + setterType, + }; + } + + get value() { + return this.field.valueState === -1 ? null : this.field.getValue(); + } + initDefaultValue() { + const { initialValue } = this.setterInfo; + if (this.state?.fromOnChange || + !isInitialValueNotEmpty(initialValue) || + this.ignoreDefaultValue || + this.value !== undefined + ) { + return; + } // 当前 field 没有 value 值时,将 initialValue 写入 field // 之所以用 initialValue,而不是 defaultValue 是为了保持跟 props.onInitial 的逻辑一致 - if (!this.state?.fromOnChange && value === undefined && isInitialValueNotEmpty(initialValue)) { - const _initialValue = typeof initialValue === 'function' ? initialValue(field.internalToShellPropEntry()) : initialValue; - field.setValue(_initialValue); - value = _initialValue; - } + const _initialValue = typeof initialValue === 'function' ? initialValue(this.field.internalToShellField()) : initialValue; + this.field.setValue(_initialValue); + } + + componentDidMount() { + this.initDefaultValue(); + } + + render() { + const field = this.field; + const { extraProps } = field; + const visible = this.visible; if (!visible) { return null; } - // 根据是否支持变量配置做相应的更改 - const supportVariable = field.extraProps?.supportVariable; - // supportVariableGlobally 只对标准组件生效,vc 需要单独配置 - const supportVariableGlobally = engineConfig.get('supportVariableGlobally', false) && isStandardComponent(componentMeta); - if (supportVariable || supportVariableGlobally) { - if (setterType === 'MixedSetter') { - // VariableSetter 不单独使用 - if (Array.isArray(setterProps.setters) && !setterProps.setters.includes('VariableSetter')) { - setterProps.setters.push('VariableSetter'); - } - } else { - setterType = 'MixedSetter'; - setterProps = { - setters: [ - setter, - 'VariableSetter', - ], - }; - } - } + const { + setterProps = {}, + setterType, + initialValue = null, + } = this.setterInfo; + + const value = this.value; - let _onChange = extraProps?.onChange; + let onChangeAPI = extraProps?.onChange; let stageName = this.stageName; return createField( @@ -171,14 +243,14 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView ...extraProps, }, !stageName && - createSetterContent(setterType, { + this.setters?.createSetterContent(setterType, { ...shallowIntl(setterProps), forceInline: extraProps.forceInline, key: field.id, // === injection - prop: field.internalToShellPropEntry(), // for compatible vision + prop: field.internalToShellField(), // for compatible vision selected: field.top?.getNode()?.internalToShellNode(), - field: field.internalToShellPropEntry(), + field: field.internalToShellField(), // === IO value, // reaction point initialValue, @@ -189,13 +261,13 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView value, }); field.setValue(value, true); - if (_onChange) _onChange(value, field); + if (onChangeAPI) onChangeAPI(value, field.internalToShellField()); }, onInitial: () => { if (initialValue == null) { return; } - const value = typeof initialValue === 'function' ? initialValue(field.internalToShellPropEntry()) : initialValue; + const value = typeof initialValue === 'function' ? initialValue(field.internalToShellField()) : initialValue; this.setState({ // eslint-disable-next-line react/no-unused-state value, @@ -204,7 +276,9 @@ class SettingFieldView extends Component<SettingFieldViewProps, SettingFieldView }, removeProp: () => { - field.parent.clearPropValue(field.name); + if (field.name) { + field.parent.clearPropValue(field.name); + } }, }), extraProps.forceInline ? 'plain' : extraProps.display, @@ -224,14 +298,14 @@ class SettingGroupView extends Component<SettingGroupViewProps> { const { field } = this.props; const { extraProps } = field; const { display } = extraProps; - const editor = globalContext.get('editor'); - const { stages } = editor.get('skeleton') as Skeleton; + const editor = this.props.field.designer?.editor; + const { stages } = editor?.get('skeleton') as Skeleton; // const items = field.items; let stageName; if (display === 'entry') { runInAction(() => { - stageName = `${field.getNode().id}_${field.name.toString()}`; + stageName = `${field.getNode().id}_${field.name?.toString()}`; // 清除原 stage,不然 content 引用的一直是老的 field,导致数据无法得到更新 stages.container.remove(stageName); stages.add({ @@ -251,7 +325,7 @@ class SettingGroupView extends Component<SettingGroupViewProps> { const { field } = this.props; const { extraProps } = field; const { condition, display } = extraProps; - const visible = field.isSingle && typeof condition === 'function' ? condition(field.internalToShellPropEntry()) !== false : true; + const visible = field.isSingle && typeof condition === 'function' ? condition(field.internalToShellField()) !== false : true; if (!visible) { return null; @@ -275,20 +349,20 @@ class SettingGroupView extends Component<SettingGroupViewProps> { } } -export function createSettingFieldView(item: SettingField | CustomView, field: SettingEntry, index?: number) { - if (isSettingField(item)) { - if (item.isGroup) { - return <SettingGroupView field={item} key={item.id} />; +export function createSettingFieldView(field: ISettingField | IPublicTypeCustomView, fieldEntry: ISettingEntry, index?: number) { + if (isSettingField(field)) { + if (field.isGroup) { + return <SettingGroupView field={field} key={field.id} />; } else { - return <SettingFieldView field={item} key={item.id} />; + return <SettingFieldView field={field} key={field.id} />; } } else { - return createContent(item, { key: index, field }); + return createContent(field, { key: index, field: fieldEntry }); } } export type SettingsPaneProps = { - target: SettingTopEntry | SettingField; + target: ISettingTopEntry | ISettingField; usePopup?: boolean; }; diff --git a/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx b/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx index c52780541e..747a2ea1df 100644 --- a/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx +++ b/packages/editor-skeleton/src/components/settings/settings-primary-pane.tsx @@ -1,24 +1,30 @@ import React, { Component } from 'react'; import { Tab, Breadcrumb } from '@alifd/next'; import { Title, observer, Editor, obx, globalContext, engineConfig, makeObservable } from '@alilc/lowcode-editor-core'; -import { Node, isSettingField, SettingField, Designer } from '@alilc/lowcode-designer'; +import { Node, SettingField, isSettingField, INode } from '@alilc/lowcode-designer'; import classNames from 'classnames'; import { SettingsMain } from './main'; import { SettingsPane } from './settings-pane'; import { StageBox } from '../stage-box'; import { SkeletonContext } from '../../context'; +import { intl } from '../../locale'; import { createIcon } from '@alilc/lowcode-utils'; +interface ISettingsPrimaryPaneProps { + engineEditor: Editor; + config: any; +} + @observer -export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any }, { shouldIgnoreRoot: boolean }> { +export class SettingsPrimaryPane extends Component<ISettingsPrimaryPaneProps, { shouldIgnoreRoot: boolean }> { state = { shouldIgnoreRoot: false, }; - private main = new SettingsMain(globalContext.get('editor')); + private main = new SettingsMain(this.props.engineEditor); @obx.ref private _activeKey?: any; - constructor(props) { + constructor(props: ISettingsPrimaryPaneProps) { super(props); makeObservable(this); } @@ -26,7 +32,9 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any componentDidMount() { this.setShouldIgnoreRoot(); - globalContext.get('editor').on('designer.selection.change', () => { + const editor = this.props.engineEditor; + + editor.eventBus.on('designer.selection.change', () => { if (!engineConfig.get('stayOnTheSameSettingTab', false)) { this._activeKey = null; } @@ -45,8 +53,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any } renderBreadcrumb() { - const { settings } = this.main; - const { config } = this.props; + const { settings, editor } = this.main; // const shouldIgnoreRoot = config.props?.ignoreRoot; const { shouldIgnoreRoot } = this.state; if (!settings) { @@ -57,19 +64,19 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any <div className="lc-settings-navigator"> {createIcon(settings.componentMeta?.icon, { className: 'lc-settings-navigator-icon', - class: 'lc-settings-navigator-icon', })} - <Title title={settings.componentMeta!.title} /> - <span> x {settings.nodes.length}</span> + <div style={{ marginLeft: '5px' }}> + <Title title={settings.componentMeta!.title} /> + <span> x {settings.nodes.length}</span> + </div> </div> ); } - const editor = globalContext.get('editor'); const designer = editor.get('designer'); const current = designer?.currentSelection?.getNodes()?.[0]; - let node: Node | null = settings.first; - const { focusNode } = node.document; + let node: INode | null = settings.first; + const focusNode = node.document?.focusNode; const items = []; let l = 3; @@ -79,7 +86,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any if (shouldIgnoreRoot && node.isRoot()) { break; } - if (node.contains(focusNode)) { + if (focusNode && node.contains(focusNode)) { l = 0; } const props = @@ -101,7 +108,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any }; const selected = getName(current); const target = getName(_node); - editor?.emit('skeleton.settingsPane.Breadcrumb', { + editor?.eventBus.emit('skeleton.settingsPane.Breadcrumb', { selected, target, }); @@ -128,13 +135,13 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any render() { const { settings } = this.main; - const editor = globalContext.get('editor'); + const editor = this.props.engineEditor; if (!settings) { // 未选中节点,提示选中 或者 显示根节点设置 return ( <div className="lc-settings-main"> <div className="lc-settings-notice"> - <p>请在左侧画布选中节点</p> + <p>{intl('Please select a node in canvas')}</p> </div> </div> ); @@ -145,7 +152,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any return ( <div className="lc-settings-main"> <div className="lc-settings-notice"> - <p>该节点已被锁定,无法配置</p> + <p>{intl('Current node is locked')}</p> </div> </div> ); @@ -154,7 +161,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any return ( <div className="lc-settings-main"> <div className="lc-settings-notice"> - <p>该组件暂无配置</p> + <p>{intl('No config found for this type of component')}</p> </div> </div> ); @@ -165,7 +172,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any return ( <div className="lc-settings-main"> <div className="lc-settings-notice"> - <p>请选中同一类型节点编辑</p> + <p>{intl('Please select same kind of components')}</p> </div> </div> ); @@ -206,7 +213,7 @@ export class SettingsPrimaryPane extends Component<{ editor: Editor; config: any key={field.name} onClick={ () => { - editor?.emit('skeleton.settingsPane.change', { + editor?.eventBus.emit('skeleton.settingsPane.change', { name: field.name, title: field.title, }); diff --git a/packages/editor-skeleton/src/components/settings/style.less b/packages/editor-skeleton/src/components/settings/style.less index 23b707afd8..4599ed55c2 100644 --- a/packages/editor-skeleton/src/components/settings/style.less +++ b/packages/editor-skeleton/src/components/settings/style.less @@ -33,14 +33,6 @@ position: relative; margin-bottom: 4px; position: absolute; - - .lc-setting-stage-back-icon { - position: absolute; - left: 8px; - top: 8px; - color: #8f9bb3; - cursor: pointer; - } } .lc-settings-notice { @@ -143,7 +135,6 @@ .lc-outline-pane { position: absolute; z-index: 100; - background-color: white; top: 0; bottom: 0; display: none; diff --git a/packages/editor-skeleton/src/components/stage-box/stage-box.tsx b/packages/editor-skeleton/src/components/stage-box/stage-box.tsx index 7be509097f..5015853824 100644 --- a/packages/editor-skeleton/src/components/stage-box/stage-box.tsx +++ b/packages/editor-skeleton/src/components/stage-box/stage-box.tsx @@ -1,10 +1,9 @@ import React, { Component } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import { SettingTopEntry, SettingField } from '@alilc/lowcode-designer'; import StageChain from './stage-chain'; import Stage from './stage'; -import { Skeleton } from '../../skeleton'; +import { ISkeleton } from '../../skeleton'; import PopupService, { PopupPipe } from '../popup'; import { Stage as StageWidget } from '../../widget/stage'; @@ -14,9 +13,7 @@ export type StageBoxProps = typeof StageBoxDefaultProps & { stageChain?: StageChain; className?: string; children: React.ReactNode; - skeleton: Skeleton; - // @todo to remove - target?: SettingTopEntry | SettingField; + skeleton: ISkeleton; }; type WillDetachMember = () => void; diff --git a/packages/editor-skeleton/src/components/stage-box/stage-chain.ts b/packages/editor-skeleton/src/components/stage-box/stage-chain.ts index 37119ec834..fe931769fa 100644 --- a/packages/editor-skeleton/src/components/stage-box/stage-chain.ts +++ b/packages/editor-skeleton/src/components/stage-box/stage-chain.ts @@ -1,13 +1,13 @@ -import { EventEmitter } from 'events'; import { Stage as StageWidget } from '../../widget/stage'; +import { createModuleEventBus, IEventBus } from '@alilc/lowcode-editor-core'; export default class StageChain { - private emitter: EventEmitter; + private emitter: IEventBus; private stage: StageWidget; constructor(stage: StageWidget) { - this.emitter = new EventEmitter(); + this.emitter = createModuleEventBus('StageChain'); this.stage = stage; } diff --git a/packages/editor-skeleton/src/components/stage-box/stage.tsx b/packages/editor-skeleton/src/components/stage-box/stage.tsx index 64209fbd5d..e4b0c0ef12 100644 --- a/packages/editor-skeleton/src/components/stage-box/stage.tsx +++ b/packages/editor-skeleton/src/components/stage-box/stage.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { IconArrow } from '../../icons/arrow'; import { IconExit } from '../../icons/exit'; import { Stage as StageWidget } from '../../widget/stage'; -import { isTitleConfig } from '@alilc/lowcode-types'; +import { isTitleConfig } from '@alilc/lowcode-utils'; export const StageDefaultProps = { current: false, diff --git a/packages/editor-skeleton/src/components/widget-views/index.tsx b/packages/editor-skeleton/src/components/widget-views/index.tsx index 14a2971a21..7cdff4c014 100644 --- a/packages/editor-skeleton/src/components/widget-views/index.tsx +++ b/packages/editor-skeleton/src/components/widget-views/index.tsx @@ -1,12 +1,11 @@ import { Component, ReactElement } from 'react'; -import { Icon } from '@alifd/next'; import classNames from 'classnames'; -import { Title, observer, Tip, globalContext, Editor } from '@alilc/lowcode-editor-core'; +import { Title, observer, HelpTip } from '@alilc/lowcode-editor-core'; import { DockProps } from '../../types'; -import PanelDock from '../../widget/panel-dock'; +import { PanelDock } from '../../widget/panel-dock'; import { composeTitle } from '../../widget/utils'; -import WidgetContainer from '../../widget/widget-container'; -import Panel from '../../widget/panel'; +import { WidgetContainer } from '../../widget/widget-container'; +import { Panel } from '../../widget/panel'; import { IWidget } from '../../widget/widget'; import { SkeletonEvents } from '../../skeleton'; import DraggableLine from '../draggable-line'; @@ -26,27 +25,10 @@ export function DockView({ title, icon, description, size, className, onClick }: ); } -function HelpTip({ tip }: any) { - if (tip && tip.url) { - return ( - <div> - <a href={tip.url} target="_blank" rel="noopener noreferrer"> - <Icon type="help" size="small" className="lc-help-tip" /> - </a> - <Tip>{tip.content}</Tip> - </div> - ); - } - return ( - <div> - <Icon type="help" size="small" className="lc-help-tip" /> - <Tip>{tip.content}</Tip> - </div> - ); -} - @observer export class PanelDockView extends Component<DockProps & { dock: PanelDock }> { + private lastActived = false; + componentDidMount() { this.checkActived(); } @@ -55,8 +37,6 @@ export class PanelDockView extends Component<DockProps & { dock: PanelDock }> { this.checkActived(); } - private lastActived = false; - checkActived() { const { dock } = this.props; if (dock.actived !== this.lastActived) { @@ -116,15 +96,15 @@ export class DraggableLineView extends Component<{ panel: Panel }> { } // 抛出事件,对于有些需要 panel 插件随着 度变化进行再次渲染的,由panel插件内部监听事件实现 - const editor = globalContext.get(Editor); - editor?.emit('dockpane.drag', width); + const editor = this.props.panel.skeleton.editor; + editor?.eventBus.emit('dockpane.drag', width); } onDragChange(type: 'start' | 'end') { - const editor = globalContext.get(Editor); - editor?.emit('dockpane.dragchange', type); + const editor = this.props.panel.skeleton.editor; + editor?.eventBus.emit('dockpane.dragchange', type); // builtinSimulator 屏蔽掉 鼠标事件 - editor?.emit('designer.builtinSimulator.disabledEvents', type === 'start'); + editor?.eventBus.emit('designer.builtinSimulator.disabledEvents', type === 'start'); } render() { @@ -132,7 +112,7 @@ export class DraggableLineView extends Component<{ panel: Panel }> { // 默认 关闭,通过配置开启 const enableDrag = this.props.panel.config.props?.enableDrag; const isRightArea = this.props.panel.config?.area === 'rightArea'; - if (isRightArea || !enableDrag || this.props.panel?.parent.name === 'leftFixedArea') { + if (isRightArea || !enableDrag || this.props.panel?.parent?.name === 'leftFixedArea') { return null; } return ( @@ -157,6 +137,8 @@ export class DraggableLineView extends Component<{ panel: Panel }> { @observer export class TitledPanelView extends Component<{ panel: Panel; area?: string }> { + private lastVisible = false; + componentDidMount() { this.checkVisible(); } @@ -165,8 +147,6 @@ export class TitledPanelView extends Component<{ panel: Panel; area?: string }> this.checkVisible(); } - private lastVisible = false; - checkVisible() { const { panel } = this.props; const currentVisible = panel.inited && panel.visible; @@ -185,9 +165,9 @@ export class TitledPanelView extends Component<{ panel: Panel; area?: string }> if (!panel.inited) { return null; } - const editor = globalContext.get(Editor); + const editor = panel.skeleton.editor; const panelName = area ? `${area}-${panel.name}` : panel.name; - editor?.emit('skeleton.panel.toggle', { + editor?.eventBus.emit('skeleton.panel.toggle', { name: panelName || '', status: panel.visible ? 'show' : 'hide', }); @@ -215,6 +195,8 @@ export class PanelView extends Component<{ hideOperationRow?: boolean; hideDragLine?: boolean; }> { + private lastVisible = false; + componentDidMount() { this.checkVisible(); } @@ -223,8 +205,6 @@ export class PanelView extends Component<{ this.checkVisible(); } - private lastVisible = false; - checkVisible() { const { panel } = this.props; const currentVisible = panel.inited && panel.visible; @@ -232,12 +212,8 @@ export class PanelView extends Component<{ this.lastVisible = currentVisible; if (this.lastVisible) { panel.skeleton.postEvent(SkeletonEvents.PANEL_SHOW, panel.name, panel); - // FIXME! remove this line - panel.skeleton.postEvent('leftPanel.show' as any, panel.name, panel); } else { panel.skeleton.postEvent(SkeletonEvents.PANEL_HIDE, panel.name, panel); - // FIXME! remove this line - panel.skeleton.postEvent('leftPanel.hide' as any, panel.name, panel); } } } @@ -247,9 +223,9 @@ export class PanelView extends Component<{ if (!panel.inited) { return null; } - const editor = globalContext.get(Editor); + const editor = panel.skeleton.editor; const panelName = area ? `${area}-${panel.name}` : panel.name; - editor?.emit('skeleton.panel.toggle', { + editor?.eventBus.emit('skeleton.panel.toggle', { name: panelName || '', status: panel.visible ? 'show' : 'hide', }); @@ -270,15 +246,28 @@ export class PanelView extends Component<{ } @observer -export class TabsPanelView extends Component<{ container: WidgetContainer<Panel> }> { +export class TabsPanelView extends Component<{ + container: WidgetContainer<Panel>; + // shouldHideSingleTab: 一个布尔值,用于控制当 Tabs 组件只有一个标签时是否隐藏该标签。 + shouldHideSingleTab?: boolean; +}> { render() { const { container } = this.props; const titles: ReactElement[] = []; const contents: ReactElement[] = []; - container.items.forEach((item: any) => { - titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />); - contents.push(<PanelView key={item.id} panel={item} hideOperationRow hideDragLine />); - }); + // 如果只有一个标签且 shouldHideSingleTab 为 true,则不显示 Tabs + if (this.props.shouldHideSingleTab && container.items.length === 1) { + contents.push(<PanelView key={container.items[0].id} panel={container.items[0]} hideOperationRow hideDragLine />); + } else { + container.items.forEach((item: any) => { + titles.push(<PanelTitle key={item.id} panel={item} className="lc-tab-title" />); + contents.push(<PanelView key={item.id} panel={item} hideOperationRow hideDragLine />); + }); + } + + if (!titles.length) { + return contents; + } return ( <div className="lc-tabs"> @@ -319,7 +308,7 @@ class PanelTitle extends Component<{ panel: Panel; className?: string }> { data-name={panel.name} > <Title title={panel.title || panel.name} /> - {panel.help ? <HelpTip tip={panel.help} /> : null} + {panel.help ? <HelpTip help={panel.help} /> : null} </div> ); } @@ -327,6 +316,9 @@ class PanelTitle extends Component<{ panel: Panel; className?: string }> { @observer export class WidgetView extends Component<{ widget: IWidget }> { + private lastVisible = false; + private lastDisabled: boolean | undefined = false; + componentDidMount() { this.checkVisible(); this.checkDisabled(); @@ -337,9 +329,6 @@ export class WidgetView extends Component<{ widget: IWidget }> { this.checkDisabled(); } - private lastVisible = false; - private lastDisabled = false; - checkVisible() { const { widget } = this.props; const currentVisible = widget.visible; diff --git a/packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx b/packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx index ab8de3cc20..605f4f5705 100644 --- a/packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx +++ b/packages/editor-skeleton/src/components/widget-views/panel-operation-row.tsx @@ -3,7 +3,7 @@ import { Button, Icon } from '@alifd/next'; import { action, makeObservable } from '@alilc/lowcode-editor-core'; import { IconFix } from '../../icons/fix'; import { IconFloat } from '../../icons/float'; -import Panel from '../../widget/panel'; +import { Panel } from '../../widget/panel'; export default class PanelOperationRow extends Component<{ panel: Panel }> { constructor(props) { diff --git a/packages/editor-skeleton/src/context.ts b/packages/editor-skeleton/src/context.ts index ee213e8861..58eb48ac61 100644 --- a/packages/editor-skeleton/src/context.ts +++ b/packages/editor-skeleton/src/context.ts @@ -1,4 +1,4 @@ import { createContext } from 'react'; -import { Skeleton } from './skeleton'; +import { ISkeleton } from './skeleton'; -export const SkeletonContext = createContext<Skeleton>({} as any); +export const SkeletonContext = createContext<ISkeleton>({} as any); diff --git a/packages/editor-skeleton/src/index.ts b/packages/editor-skeleton/src/index.ts index ac4dd14fb0..a39b445d1f 100644 --- a/packages/editor-skeleton/src/index.ts +++ b/packages/editor-skeleton/src/index.ts @@ -1,3 +1,4 @@ +export * from './area'; export { Workbench } from './layouts/workbench'; export * from './skeleton'; export * from './types'; @@ -6,3 +7,5 @@ export * from './components/field'; export * from './components/popup'; export * from './context'; export * from './register-defaults'; +export * from './widget'; +export * from './layouts'; diff --git a/packages/editor-skeleton/src/layouts/bottom-area.tsx b/packages/editor-skeleton/src/layouts/bottom-area.tsx index 00004e7ec1..2981cc5969 100644 --- a/packages/editor-skeleton/src/layouts/bottom-area.tsx +++ b/packages/editor-skeleton/src/layouts/bottom-area.tsx @@ -1,8 +1,8 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; -import Panel from '../widget/panel'; +import { Area } from '../area'; +import { Panel } from '../widget/panel'; @observer export default class BottomArea extends Component<{ area: Area<any, Panel> }> { diff --git a/packages/editor-skeleton/src/layouts/index.ts b/packages/editor-skeleton/src/layouts/index.ts new file mode 100644 index 0000000000..b3c7a10580 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/index.ts @@ -0,0 +1,7 @@ +export { default as LeftArea } from './left-area'; +export { default as LeftFloatPane } from './left-float-pane'; +export { default as LeftFixedPane } from './left-fixed-pane'; +export { default as MainArea } from './main-area'; +export { default as BottomArea } from './bottom-area'; +export { default as TopArea } from './top-area'; +export { default as SubTopArea } from './sub-top-area'; \ No newline at end of file diff --git a/packages/editor-skeleton/src/layouts/left-area.tsx b/packages/editor-skeleton/src/layouts/left-area.tsx index b63a896a56..d30dcfa863 100644 --- a/packages/editor-skeleton/src/layouts/left-area.tsx +++ b/packages/editor-skeleton/src/layouts/left-area.tsx @@ -1,14 +1,17 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; +import { Area } from '../area'; @observer -export default class LeftArea extends Component<{ area: Area }> { +export default class LeftArea extends Component<{ area: Area; className?: string }> { render() { - const { area } = this.props; + const { area, className = 'lc-left-area' } = this.props; + if (area.isEmpty()) { + return null; + } return ( - <div className={classNames('lc-left-area', { + <div className={classNames(className, { 'lc-area-visible': area.visible, })} > @@ -18,7 +21,6 @@ export default class LeftArea extends Component<{ area: Area }> { } } - @observer class Contents extends Component<{ area: Area }> { render() { diff --git a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx index 1492e168b8..a56b449079 100644 --- a/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx +++ b/packages/editor-skeleton/src/layouts/left-fixed-pane.tsx @@ -1,18 +1,17 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; -import { PanelConfig } from '../types'; -import Panel from '../widget/panel'; +import { Area } from '../area'; +import { Panel } from '../widget/panel'; +import { IPublicTypePanelConfig } from '@alilc/lowcode-types'; @observer -export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, Panel> }> { +export default class LeftFixedPane extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> { componentDidUpdate() { // FIXME: dirty fix, need deep think this.props.area.skeleton.editor.get('designer')?.touchOffsetObserver(); } - render() { const { area } = this.props; const width = area.current?.config.props?.width; @@ -36,7 +35,7 @@ export default class LeftFixedPane extends Component<{ area: Area<PanelConfig, P } @observer -class Contents extends Component<{ area: Area<PanelConfig, Panel> }> { +class Contents extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> { render() { const { area } = this.props; return <Fragment>{area.container.items.map((panel) => panel.content)}</Fragment>; diff --git a/packages/editor-skeleton/src/layouts/left-float-pane.tsx b/packages/editor-skeleton/src/layouts/left-float-pane.tsx index 4d7fa1e2b8..296f083211 100644 --- a/packages/editor-skeleton/src/layouts/left-float-pane.tsx +++ b/packages/editor-skeleton/src/layouts/left-float-pane.tsx @@ -1,11 +1,12 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; -import { observer, Focusable, focusTracker } from '@alilc/lowcode-editor-core'; -import Area from '../area'; -import Panel from '../widget/panel'; +import { observer, Focusable } from '@alilc/lowcode-editor-core'; +import { Area } from '../area'; +import { Panel } from '../widget/panel'; +import { IPublicApiProject, IPublicTypePanelConfig } from '@alilc/lowcode-types'; @observer -export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> { +export default class LeftFloatPane extends Component<{ area: Area<IPublicTypePanelConfig, Panel> }> { private dispose?: () => void; private focusing?: Focusable; @@ -24,13 +25,15 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> if (panelElem) return; area.setVisible(false); }; - area.skeleton.editor.on('designer.drag', triggerClose); + area.skeleton.editor.eventBus.on('designer.drag', triggerClose); this.dispose = () => { area.skeleton.editor.removeListener('designer.drag', triggerClose); }; - this.focusing = focusTracker.create({ + const project: IPublicApiProject | undefined = area.skeleton.editor.get('project'); + + this.focusing = area.skeleton.focusTracker.create({ range: (e) => { const target = e.target as HTMLElement; if (!target) { @@ -43,6 +46,9 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> if ((document.querySelector('.lc-simulator-content-frame') as HTMLIFrameElement)?.contentWindow?.document.documentElement.contains(target)) { return false; } + if (project?.simulatorHost?.contentWindow?.document.documentElement.contains(target)) { + return false; + } // 点击设置区 if (document.querySelector('.lc-right-area')?.contains(target)) { return false; @@ -65,7 +71,6 @@ export default class LeftFloatPane extends Component<{ area: Area<any, Panel> }> this.props.area.setVisible(false); }, onBlur: () => { - // debugger this.props.area.setVisible(false); }, }); diff --git a/packages/editor-skeleton/src/layouts/main-area.tsx b/packages/editor-skeleton/src/layouts/main-area.tsx index d3e480e65f..400d337952 100644 --- a/packages/editor-skeleton/src/layouts/main-area.tsx +++ b/packages/editor-skeleton/src/layouts/main-area.tsx @@ -1,9 +1,9 @@ import { Component } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; -import Panel from '../widget/panel'; -import Widget from '../widget/widget'; +import { Area } from '../area'; +import { Panel } from '../widget/panel'; +import { Widget } from '../widget/widget'; @observer export default class MainArea extends Component<{ area: Area<any, Panel | Widget> }> { diff --git a/packages/editor-skeleton/src/layouts/right-area.tsx b/packages/editor-skeleton/src/layouts/right-area.tsx index f392699534..f00ae1461a 100644 --- a/packages/editor-skeleton/src/layouts/right-area.tsx +++ b/packages/editor-skeleton/src/layouts/right-area.tsx @@ -1,13 +1,16 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; -import Panel from '../widget/panel'; +import { Area } from '../area'; +import { Panel } from '../widget/panel'; @observer export default class RightArea extends Component<{ area: Area<any, Panel> }> { render() { const { area } = this.props; + if (area.isEmpty()) { + return null; + } return ( <div className={classNames('lc-right-area engine-tabpane', { 'lc-area-visible': area.visible, @@ -19,14 +22,23 @@ export default class RightArea extends Component<{ area: Area<any, Panel> }> { } } - @observer class Contents extends Component<{ area: Area<any, Panel> }> { render() { const { area } = this.props; + return ( <Fragment> - {area.container.items.map((item) => item.content)} + { + area.container.items + .slice() + .sort((a, b) => { + const index1 = a.config?.index || 0; + const index2 = b.config?.index || 0; + return index1 === index2 ? 0 : (index1 > index2 ? 1 : -1); + }) + .map((item) => item.content) + } </Fragment> ); } diff --git a/packages/editor-skeleton/src/layouts/sub-top-area.tsx b/packages/editor-skeleton/src/layouts/sub-top-area.tsx new file mode 100644 index 0000000000..cef6aa5b02 --- /dev/null +++ b/packages/editor-skeleton/src/layouts/sub-top-area.tsx @@ -0,0 +1,67 @@ +import { Component, Fragment } from 'react'; +import classNames from 'classnames'; +import { observer } from '@alilc/lowcode-editor-core'; +import { Area } from '@alilc/lowcode-editor-skeleton'; + +@observer +export default class SubTopArea extends Component<{ area: Area; itemClassName?: string }> { + render() { + const { area, itemClassName } = this.props; + + if (area.isEmpty()) { + return null; + } + + return ( + <div className={classNames('lc-workspace-sub-top-area lc-sub-top-area engine-actionpane', { + 'lc-area-visible': area.visible, + })} + > + <Contents area={area} itemClassName={itemClassName} /> + </div> + ); + } +} + +@observer +class Contents extends Component<{ area: Area; itemClassName?: string }> { + render() { + const { area, itemClassName } = this.props; + const left: any[] = []; + const center: any[] = []; + const right: any[] = []; + area.container.items.slice().sort((a, b) => { + const index1 = a.config?.index || 0; + const index2 = b.config?.index || 0; + return index1 === index2 ? 0 : (index1 > index2 ? 1 : -1); + }).forEach(item => { + const content = ( + <div className={itemClassName || ''} key={`top-area-${item.name}`}> + {item.content} + </div> + ); + if (item.align === 'center') { + center.push(content); + } else if (item.align === 'left') { + left.push(content); + } else { + right.push(content); + } + }); + let children = []; + if (left && left.length) { + children.push(<div className="lc-workspace-sub-top-area-left lc-sub-top-area-left">{left}</div>); + } + if (center && center.length) { + children.push(<div className="lc-workspace-sub-top-area-center lc-sub-top-area-center">{center}</div>); + } + if (right && right.length) { + children.push(<div className="lc-workspace-sub-top-area-right lc-sub-top-area-right">{right}</div>); + } + return ( + <Fragment> + {children} + </Fragment> + ); + } +} diff --git a/packages/editor-skeleton/src/layouts/theme.less b/packages/editor-skeleton/src/layouts/theme.less index b0da053db1..01542c7584 100644 --- a/packages/editor-skeleton/src/layouts/theme.less +++ b/packages/editor-skeleton/src/layouts/theme.less @@ -14,8 +14,11 @@ --color-icon-normal: @normal-alpha-4; --color-icon-hover: @normal-alpha-3; + --color-icon-light: @normal-alpha-5; --color-icon-active: @brand-color-1; --color-icon-reverse: @white-alpha-1; + --color-icon-disabled: @normal-alpha-6; + --color-icon-pane: @dark-alpha-3; --color-line-normal: @normal-alpha-7; --color-line-darken: darken(@normal-alpha-7, 10%); @@ -25,7 +28,7 @@ --color-text-dark: darken(@dark-alpha-3, 10%); --color-text-light: lighten(@dark-alpha-3, 10%); --color-text-reverse: @white-alpha-2; - --color-text-regular: @normal-alpha-2; + --color-text-disabled: @gray-light; --color-field-label: @dark-alpha-4; --color-field-text: @dark-alpha-3; @@ -35,6 +38,44 @@ --color-field-border-active: @normal-alpha-3; --color-field-background: @white-alpha-1; + --color-success: @brand-success; + --colo-success-dark: darken(@brand-success, 10%); + --color-success-light: lighten(@brand-success, 10%); + --color-warning: @brand-warning; + --color-warning-dark: darken(@brand-warning, 10%); + --color-warning-light: lighten(@brand-warning, 10%); + --color-information: @brand-link-hover; + --color-information-dark: darken(@brand-link-hover, 10%); + --color-information-light: lighten(@brand-link-hover, 10%); + --color-error: @brand-danger; + --color-error-dark: darken(@brand-danger, 10%); + --color-error-light: lighten(@brand-danger, 10%); + --color-purple: rgb(144, 94, 190); + --color-brown: #7b605b; + + --color-pane-background: @white-alpha-1; + --color-block-background-normal: @white-alpha-1; + --color-block-background-light: @normal-alpha-9; + --color-block-background-dark: @normal-alpha-7; + --color-block-background-shallow: @normal-alpha-8; + --color-block-background-disabled: @normal-alpha-6; + --color-block-background-active: @brand-color-1; + --color-block-background-active-light: @brand-color-1-7; + --color-block-background-warning: @brand-warning-alpha-7; + --color-block-background-error: @brand-danger-alpha-7; + --color-block-background-success: @brand-success-alpha-7; + --color-block-background-deep-dark: @normal-5; + --color-layer-mask-background: @dark-alpha-7; + --color-layer-tooltip-background: rgba(44,47,51,0.8); + --color-background: #edeff3; + + --color-canvas-detecting-background: rgba(0,121,242,.04); + + --pane-title-bg-color: rgba(31,56,88,.04); +} + +// @deprecated 变量 +:root { --color-function-success: @brand-success; --color-function-success-dark: darken(@brand-success, 10%); --color-function-success-light: lighten(@brand-success, 10%); @@ -47,16 +88,7 @@ --color-function-error: @brand-danger; --color-function-error-dark: darken(@brand-danger, 10%); --color-function-error-light: lighten(@brand-danger, 10%); - - --color-pane-background: @white-alpha-1; - --color-block-background-normal: @white-alpha-1; - --color-block-background-light: @normal-alpha-9; - --color-block-background-shallow: @normal-alpha-8; - --color-block-background-dark: @normal-alpha-7; - --color-block-background-disabled: @normal-alpha-6; - --color-block-background-deep-dark: @normal-5; - --color-layer-mask-background: @dark-alpha-7; - --color-layer-tooltip-background: rgba(44,47,51,0.8); - - --pane-title-bg-color: rgba(31,56,88,.04); + --color-function-purple: rgb(144, 94, 190); + --color-function-brown: #7b605b; + --color-text-regular: @normal-alpha-2; } diff --git a/packages/editor-skeleton/src/layouts/toolbar.tsx b/packages/editor-skeleton/src/layouts/toolbar.tsx index 2f91efff94..9b7d1c9b68 100644 --- a/packages/editor-skeleton/src/layouts/toolbar.tsx +++ b/packages/editor-skeleton/src/layouts/toolbar.tsx @@ -1,7 +1,7 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; +import { Area } from '../area'; @observer export default class Toolbar extends Component<{ area: Area }> { diff --git a/packages/editor-skeleton/src/layouts/top-area.tsx b/packages/editor-skeleton/src/layouts/top-area.tsx index a28a020060..f6b84b3e6f 100644 --- a/packages/editor-skeleton/src/layouts/top-area.tsx +++ b/packages/editor-skeleton/src/layouts/top-area.tsx @@ -1,14 +1,17 @@ import { Component, Fragment } from 'react'; import classNames from 'classnames'; import { observer } from '@alilc/lowcode-editor-core'; -import Area from '../area'; +import { Area } from '../area'; @observer -export default class TopArea extends Component<{ area: Area; itemClassName?: string }> { +export default class TopArea extends Component<{ area: Area; itemClassName?: string; className?: string }> { render() { - const { area, itemClassName } = this.props; + const { area, itemClassName, className } = this.props; + if (area.isEmpty()) { + return null; + } return ( - <div className={classNames('lc-top-area engine-actionpane', { + <div className={classNames(className, 'lc-top-area engine-actionpane', { 'lc-area-visible': area.visible, })} > diff --git a/packages/editor-skeleton/src/layouts/workbench.less b/packages/editor-skeleton/src/layouts/workbench.less index 02ff36966d..9c7a9815dd 100644 --- a/packages/editor-skeleton/src/layouts/workbench.less +++ b/packages/editor-skeleton/src/layouts/workbench.less @@ -13,6 +13,7 @@ --popup-border-radius: @popup-border-radius; --left-area-width: 48px; + --workspace-left-area-width: 48px; --right-area-width: 300px; --top-area-height: 48px; --toolbar-height: 36px; @@ -38,7 +39,7 @@ body { font-family: var(--font-family); font-size: var(--font-size-text); color: var(--color-text); - background-color: #edeff3; + background-color: var(--color-background); } * { @@ -49,13 +50,10 @@ body { width: 100%; height: 100%; position: relative; - background-color: #fff; - background-color: var(--color-pane-background); &.hidden { display: none; } .lc-panel-title { - // background-color: var(--pane-title-bg-color,rgba(31,56,88,.04)); display: flex; align-items: center; justify-content: flex-start; @@ -63,59 +61,25 @@ body { .lc-help-tip { margin-left: 4px; - color: rgba(0, 0, 0, 0.4); + color: var(--color-icon-normal, rgba(0, 0, 0, 0.4)); cursor: pointer; } } > .lc-panel-title { - height: 48px; - font-size: 16px; - padding: 0 15px; - // border-bottom: 1px solid var(--color-line-normal,rgba(31,56,88,.1)); - color: #0f1726; + height: var(--pane-title-height, 48px); + font-size: var(--pane-title-font-size, 16px); + padding: var(--pane-title-padding, 0 15px); + color: var(--color-title, #0f1726); font-weight: bold; } .lc-panel-body { position: absolute; - top: 48px; + top: var(--pane-title-height, 48px); bottom: 0; left: 0; right: 0; overflow: visible; - /* - .my-tabs { - width: 100%; - height: 100%; - position: relative; - .tabs-title { - display: flex; - height: var(--pane-title-height); - > .tab-title { - cursor: pointer; - padding: 0; - flex: 1; - min-width: 0; - justify-content: center; - border-bottom: 2px solid transparent; - &.actived { - cursor: default; - color: var(--color-text-avtived); - border-bottom-color: #3896ee; - } - } - } - .tabs-content { - position: absolute; - top: var(--pane-title-height); - bottom: 0; - left: 0; - right: 0; - height: calc(100% - var(--pane-title-height)); - overflow: hidden; - } - } - */ } .lc-outline-tree-container { border-top: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1)); @@ -125,43 +89,99 @@ body { height: 100%; width: 100%; position: relative; - background-color: #fff; - background-color: var(--color-pane-background); - // overflow: auto; &.hidden { display: none; } } -.lc-workbench { +.workspace-engine-main { + height: 100%; + display: flex; + flex-direction: column; + background-color: var(--color-background); + position: absolute; + left: 0; + top: 0; + right: 0; + bottom: 0; + z-index: -1; + overflow: hidden; + + &.active { + z-index: 999; + } + + .lc-workbench { + + } + + .engine-editor-view { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + + &.active { + z-index: 999; + background: var(--color-background); + } + } +} + +.lc-workbench, .lc-workspace-workbench { height: 100%; display: flex; flex-direction: column; - background-color: #edeff3; - .lc-top-area { - height: var(--top-area-height); - background-color: var(--color-pane-background); + background-color: var(--color-background); + + &.engine-main { + height: 100%; + display: flex; + flex-direction: column; + background-color: var(--color-background); + } + .lc-top-area, .lc-workspace-sub-top-area { width: 100%; display: none; margin-bottom: 2px; padding: 8px 12px 8px 16px; + &.lc-top-area { + background-color: var(--color-top-area-background, var(--color-pane-background)); + height: var(--top-area-height); + } + + &.lc-workspace-top-area { + background-color: var(--color-workspace-top-area-background, var(--color-pane-background)); + } + + &.lc-workspace-sub-top-area { + background-color: var(--color-workspace-sub-top-area-background, var(--color-pane-background)); + height: var(--workspace-sub-top-area-height, var(--top-area-height)); + margin: var(--workspace-sub-top-area-margin, 0px 0px 2px 0px); + padding: var(--workspace-sub-top-area-padding, 8px 12px 8px 16px); + } + &.lc-area-visible { display: flex; } - .lc-top-area-left { + .lc-top-area-left, .lc-workspace-sub-top-area-left { display: flex; align-items: center; + max-width: 100%; } - .lc-top-area-center { + .lc-top-area-center, .lc-workspace-sub-top-area-center { flex: 1; display: flex; justify-content: center; margin: 0 8px; } - .lc-top-area-right { + .lc-top-area-right, .lc-workspace-sub-top-area-right { display: flex; align-items: center; > * { @@ -173,7 +193,7 @@ body { } } } - .lc-workbench-body { + .lc-workbench-body, .lc-workspace-workbench-body { flex: 1; display: flex; min-height: 0; @@ -187,8 +207,7 @@ body { display: flex; justify-content: center; align-items: center; - // background: rgba(31,56,88,0.04); - border-bottom: 1px solid #edeff3; + border-bottom: 1px solid var(--color-line-normal, #edeff3); .lc-tab-title { flex: 1; height: 32px; @@ -199,8 +218,8 @@ body { cursor: pointer; font-size: 12px; &.actived { - color: #0079f2; - border-bottom-color: #0079f2; + color: var(--color-brand, #0079f2); + border-bottom-color: var(--color-brand, #0079f2); } } } @@ -216,12 +235,12 @@ body { .lc-pane-icon-close { position: absolute; right: 16px; - top: 14px; + top: calc(var(--pane-title-height, 48px) / 2 - 10px); height: auto; z-index: 2; .next-icon { line-height: 1; - color: rgba(0, 0, 0, 0.6); + color: var(--color-icon-pane); } } @@ -229,12 +248,12 @@ body { .lc-pane-icon-float { position: absolute; right: 38px; - top: 14px; + top: calc(var(--pane-title-height, 48px) / 2 - 10px); height: auto; z-index: 2; svg { vertical-align: middle; - color: rgba(0, 0, 0, 0.6); + color: var(--color-icon-pane); } } @@ -245,8 +264,8 @@ body { width: var(--dock-pane-width); // min-width: var(--dock-fixed-pane-width); left: calc(var(--left-area-width) + 1px); - background-color: var(--color-pane-background); - box-shadow: 4px 6px 6px 0 rgba(31, 50, 88, 0.08); + background-color: var(--color-left-float-pane-background, var(--color-pane-background)); + box-shadow: 4px 6px 6px 0 var(--color-block-background-shallow, rgba(31, 50, 88, 0.08)); z-index: 820; display: none; // padding-top: 36px; @@ -254,15 +273,19 @@ body { display: block; } } - .lc-left-area { + .lc-left-area, .lc-workspace-left-area { height: 100%; - width: var(--left-area-width); - background-color: var(--color-pane-background); + width: var(--workspace-left-area-width, --left-area-width); display: none; flex-shrink: 0; flex-direction: column; justify-content: space-between; overflow: hidden; + background-color: var(--color-left-area-background, var(--color-pane-background)); + + &.lc-workspace-left-area { + background-color: var(--color-workspace-left-area-background, var(--color-pane-background)); + } &.lc-area-visible { display: flex; } @@ -273,19 +296,22 @@ body { flex-direction: column; justify-content: flex-start; align-items: center; + color: var(--color-text); + .lc-title { flex-direction: column; - width: 46px; + width: calc(var(--left-area-width) - 2px); height: 46px; display: flex; align-items: center; justify-content: center; + cursor: pointer; &.has-tip { cursor: pointer; } &.actived { - color: #0079f2; + color: var(--color-brand, #0079f2); } &.disabled { opacity: 0.4; @@ -325,6 +351,9 @@ body { .lc-left-area.lc-area-visible ~ .lc-workbench-center { margin-left: 2px; } + .lc-workspace-left-area.lc-area-visible ~ .lc-workspace-workbench-center { + margin-left: 2px; + } .lc-outline-pane { .lc-outline-tree .tree-node .tree-node-title { border-bottom: none; @@ -334,12 +363,12 @@ body { flex: 1; display: flex; flex-direction: column; - z-index: 10; + .lc-toolbar { display: flex; height: var(--toolbar-height); - background-color: var(--color-pane-background); - padding: 8px 16px; + background-color: var(--color-toolbar-background, var(--color-pane-background)); + padding: var(--toolbar-padding, 8px 16px); .lc-toolbar-center { display: flex; justify-content: center; @@ -349,6 +378,7 @@ body { } .lc-main-area { flex: 1; + background-color: var(--color-background); } .lc-bottom-area { height: var(--bottom-area-height); @@ -362,15 +392,17 @@ body { .lc-right-area { height: 100%; width: var(--right-area-width); - background-color: var(--color-pane-background); + background-color: var(--color-right-area-background, var(--color-pane-background)); display: none; flex-shrink: 0; margin-left: 2px; position: relative; > .lc-panel { position: absolute; + background-color: var(--color-right-area-background, var(--color-pane-background, #fff)); left: 0; top: 0; + z-index: 1; } &.lc-area-visible { display: block; @@ -395,4 +427,72 @@ body { } } } + .engine-actionitem { + max-width: 100%; + color: var(--color-text); + } +} + +.lc-workspace-workbench { + height: 100%; + display: flex; + flex-direction: column; + background-color: var(--color-background); + .lc-workspace-workbench-body { + flex: 1; + display: flex; + min-height: 0; + position: relative; + + > .lc-left-float-pane { + left: calc(var(--workspace-left-area-width, var(--left-area-width)) + 1px); + } + + .lc-workspace-workbench-center { + flex: 1; + display: flex; + flex-direction: column; + z-index: 10; + position: relative; + .lc-toolbar { + display: flex; + height: var(--toolbar-height); + background-color: var(--color-toolbar-background, var(--color-pane-background)); + padding: var(--toolbar-padding, 8px 16px); + .lc-toolbar-center { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + } + } + .lc-main-area { + flex: 1; + } + .lc-bottom-area { + height: var(--bottom-area-height); + background-color: var(--color-pane-background); + display: none; + &.lc-area-visible { + display: block; + } + } + } + + .lc-workspace-workbench-center-content { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + flex-direction: column; + display: flex; + align-content: stretch; + } + + .lc-workspace-workbench-window { + position: relative; + height: 100%; + } + } } diff --git a/packages/editor-skeleton/src/layouts/workbench.tsx b/packages/editor-skeleton/src/layouts/workbench.tsx index 94781fb786..1e412ed678 100644 --- a/packages/editor-skeleton/src/layouts/workbench.tsx +++ b/packages/editor-skeleton/src/layouts/workbench.tsx @@ -1,7 +1,7 @@ import { Component } from 'react'; import { TipContainer, observer } from '@alilc/lowcode-editor-core'; import classNames from 'classnames'; -import { Skeleton } from '../skeleton'; +import { ISkeleton } from '../skeleton'; import TopArea from './top-area'; import LeftArea from './left-area'; import LeftFixedPane from './left-fixed-pane'; @@ -15,19 +15,25 @@ import { SkeletonContext } from '../context'; import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types'; @observer -export class Workbench extends Component<{ skeleton: Skeleton; config?: EditorConfig; components?: PluginClassSet; className?: string; topAreaItemClassName?: string }> { +export class Workbench extends Component<{ + skeleton: ISkeleton; + config?: EditorConfig; + components?: PluginClassSet; + className?: string; + topAreaItemClassName?: string; +}> { constructor(props: any) { super(props); const { config, components, skeleton } = this.props; skeleton.buildFromConfig(config, components); } - // componentDidCatch(error: any) { - // globalContext.get(Editor).emit('editor.skeleton.workbench.error', error); - // } - render() { - const { skeleton, className, topAreaItemClassName } = this.props; + const { + skeleton, + className, + topAreaItemClassName, + } = this.props; return ( <div className={classNames('lc-workbench', className)}> <SkeletonContext.Provider value={this.props.skeleton}> diff --git a/packages/editor-skeleton/src/less-variables.less b/packages/editor-skeleton/src/less-variables.less index c44fc196e2..017e432ce6 100644 --- a/packages/editor-skeleton/src/less-variables.less +++ b/packages/editor-skeleton/src/less-variables.less @@ -99,19 +99,19 @@ @brand-link-hover: #2e76a6; // F1-1-7 A10 -@brand-danger-alpha-7: rgba(240, 70, 49, 0.9); +@brand-danger-alpha-7: rgba(240, 70, 49, 0.1); // F1-1-8 A6 @brand-danger-alpha-8: rgba(240, 70, 49, 0.8); // F2-1-2 A80 @brand-warning-alpha-2: rgba(250, 189, 14, 0.8); // F2-1-7 A10 -@brand-warning-alpha-7: rgba(250, 189, 14, 0.9); +@brand-warning-alpha-7: rgba(250, 189, 14, 0.1); // F3-1-2 A80 @brand-success-alpha-2: rgba(102, 188, 92, 0.8); // F3-1-7 A10 -@brand-success-alpha-7: rgba(102, 188, 92, 0.9); +@brand-success-alpha-7: rgba(102, 188, 92, 0.1); // F4-1-7 A10 -@brand-link-alpha-7: rgba(102, 188, 92, 0.9); +@brand-link-alpha-7: rgba(102, 188, 92, 0.1); // 文本色 @text-primary-color: @dark-alpha-3; diff --git a/packages/editor-skeleton/src/locale/en-US.json b/packages/editor-skeleton/src/locale/en-US.json index 571c555d73..36abb6395f 100644 --- a/packages/editor-skeleton/src/locale/en-US.json +++ b/packages/editor-skeleton/src/locale/en-US.json @@ -5,5 +5,11 @@ "Multiple Value, Click to Clear": "Multiple Value, Click to Clear", "Required": "Required", "Setted Value, Click to Clear": "Setted Value, Click to Clear", - "Multiple Value": "Multiple Value" + "Multiple Value": "Multiple Value", + "Attribute: ": "Attribute: ", + "Description: ": "Description: ", + "Please select a node in canvas": "Please select a node in canvas", + "Current node is locked": "Current node is locked", + "No config found for this type of component": "No config found for this type of component", + "Please select same kind of components": "Please select same kind of components" } diff --git a/packages/editor-skeleton/src/locale/index.ts b/packages/editor-skeleton/src/locale/index.ts index a912240fa3..4cb3b53cfb 100644 --- a/packages/editor-skeleton/src/locale/index.ts +++ b/packages/editor-skeleton/src/locale/index.ts @@ -1,10 +1,10 @@ import { createIntl } from '@alilc/lowcode-editor-core'; -import en_US from './en-US.json'; -import zh_CN from './zh-CN.json'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; const { intl, intlNode, getLocale, setLocale } = createIntl({ - 'en-US': en_US, - 'zh-CN': zh_CN, + 'en-US': enUS, + 'zh-CN': zhCN, }); export { intl, intlNode, getLocale, setLocale }; diff --git a/packages/editor-skeleton/src/locale/zh-CN.json b/packages/editor-skeleton/src/locale/zh-CN.json index 0ed161004b..c347ad20d2 100644 --- a/packages/editor-skeleton/src/locale/zh-CN.json +++ b/packages/editor-skeleton/src/locale/zh-CN.json @@ -1,9 +1,15 @@ { - "Binded: {expr}": "已绑定: {expr}", + "Binded: {expr}": "已绑定:{expr}", "Variable Binding": "变量绑定", "Switch Setter": "切换设置器", - "Multiple Value, Click to Clear": "多种值, 点击清除", + "Multiple Value, Click to Clear": "多种值,点击清除", "Required": "必填项", "Setted Value, Click to Clear": "已设置值,点击清除", - "Multiple Value": "多种值" + "Multiple Value": "多种值", + "Attribute: ": "属性:", + "Description: ": "说明:", + "Please select a node in canvas": "请在左侧画布选中节点", + "Current node is locked": "该节点已被锁定,无法配置", + "No config found for this type of component": "该组件暂无配置", + "Please select same kind of components": "请选中同一类型节点编辑" } diff --git a/packages/editor-skeleton/src/register-defaults.ts b/packages/editor-skeleton/src/register-defaults.ts index bb35b277d1..573631f78e 100644 --- a/packages/editor-skeleton/src/register-defaults.ts +++ b/packages/editor-skeleton/src/register-defaults.ts @@ -1,15 +1,23 @@ -import { registerMetadataTransducer } from '@alilc/lowcode-designer'; import parseJSFunc from './transducers/parse-func'; import parseProps from './transducers/parse-props'; import addonCombine from './transducers/addon-combine'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; -export const registerDefaults = () => { - // parseFunc - registerMetadataTransducer(parseJSFunc, 1, 'parse-func'); +export const registerDefaults = (ctx: IPublicModelPluginContext) => { + const { material } = ctx; + return { + init() { + // parseFunc + material.registerMetadataTransducer(parseJSFunc, 1, 'parse-func'); - // parseProps - registerMetadataTransducer(parseProps, 5, 'parse-props'); + // parseProps + material.registerMetadataTransducer(parseProps, 5, 'parse-props'); - // addon/platform custom - registerMetadataTransducer(addonCombine, 10, 'combine-props'); + // addon/platform custom + material.registerMetadataTransducer(addonCombine, 10, 'combine-props'); + }, + }; }; + + +registerDefaults.pluginName = '___register_defaults___'; diff --git a/packages/editor-skeleton/src/skeleton.ts b/packages/editor-skeleton/src/skeleton.ts index bdca4e284c..7ff462391d 100644 --- a/packages/editor-skeleton/src/skeleton.ts +++ b/packages/editor-skeleton/src/skeleton.ts @@ -1,9 +1,7 @@ -import { Editor, action, makeObservable } from '@alilc/lowcode-editor-core'; +import { action, makeObservable, obx, engineConfig, IEditor, FocusTracker } from '@alilc/lowcode-editor-core'; import { DockConfig, - PanelConfig, WidgetConfig, - IWidgetBaseConfig, PanelDockConfig, DialogDockConfig, isDockConfig, @@ -11,19 +9,29 @@ import { isPanelConfig, DividerConfig, isDividerConfig, - IWidgetConfigArea, } from './types'; -import Panel, { isPanel } from './widget/panel'; -import WidgetContainer from './widget/widget-container'; -import Area from './area'; -import Widget, { isWidget, IWidget } from './widget/widget'; -import PanelDock from './widget/panel-dock'; -import Dock from './widget/dock'; +import { isPanel, Panel } from './widget/panel'; +import { WidgetContainer } from './widget/widget-container'; +import { Area } from './area'; +import { isWidget, IWidget, Widget } from './widget/widget'; +import { PanelDock } from './widget/panel-dock'; +import { Dock } from './widget/dock'; import { Stage, StageConfig } from './widget/stage'; import { isValidElement } from 'react'; -import { isPlainObject, uniqueId } from '@alilc/lowcode-utils'; +import { isPlainObject, uniqueId, Logger } from '@alilc/lowcode-utils'; import { Divider } from '@alifd/next'; -import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types'; +import { + EditorConfig, + PluginClassSet, + IPublicTypeWidgetBaseConfig, + IPublicTypeWidgetConfigArea, + IPublicTypeSkeletonConfig, + IPublicApiSkeleton, + IPublicTypeConfigTransducer, + IPublicTypePanelConfig, +} from '@alilc/lowcode-types'; + +const logger = new Logger({ level: 'warn', bizName: 'skeleton' }); export enum SkeletonEvents { PANEL_DOCK_ACTIVE = 'skeleton.panel-dock.active', @@ -36,30 +44,103 @@ export enum SkeletonEvents { WIDGET_ENABLE = 'skeleton.widget.enable', } -export class Skeleton { +export interface ISkeleton extends Omit<IPublicApiSkeleton, + 'showPanel' | + 'hidePanel' | + 'showWidget' | + 'enableWidget' | + 'hideWidget' | + 'disableWidget' | + 'showArea' | + 'onShowPanel' | + 'onHidePanel' | + 'onShowWidget' | + 'onHideWidget' | + 'remove' | + 'hideArea' | + 'add' +> { + editor: IEditor; + + readonly leftArea: Area<DockConfig | PanelDockConfig | DialogDockConfig>; + + readonly topArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + + readonly subTopArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + + readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + + readonly leftFixedArea: Area<IPublicTypePanelConfig, Panel>; + + readonly leftFloatArea: Area<IPublicTypePanelConfig, Panel>; + + readonly rightArea: Area<IPublicTypePanelConfig, Panel>; + + readonly mainArea: Area<WidgetConfig | IPublicTypePanelConfig, Widget | Panel>; + + readonly bottomArea: Area<IPublicTypePanelConfig, Panel>; + + readonly stages: Area<StageConfig, Stage>; + + readonly widgets: IWidget[]; + + readonly focusTracker: FocusTracker; + + getPanel(name: string): Panel | undefined; + + getWidget(name: string): IWidget | undefined; + + buildFromConfig(config?: EditorConfig, components?: PluginClassSet): void; + + createStage(config: any): string | undefined; + + getStage(name: string): Stage | null; + + createContainer( + name: string, + handle: (item: any) => any, + exclusive?: boolean, + checkVisible?: () => boolean, + defaultSetCurrent?: boolean, + ): WidgetContainer; + + createPanel(config: IPublicTypePanelConfig): Panel; + + add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IWidget | Widget | Panel | Stage | Dock | PanelDock | undefined; +} + +export class Skeleton implements ISkeleton { private panels = new Map<string, Panel>(); + private configTransducers: IPublicTypeConfigTransducer[] = []; + private containers = new Map<string, WidgetContainer<any>>(); readonly leftArea: Area<DockConfig | PanelDockConfig | DialogDockConfig>; readonly topArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + readonly subTopArea: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; + readonly toolbar: Area<DockConfig | DividerConfig | PanelDockConfig | DialogDockConfig>; - readonly leftFixedArea: Area<PanelConfig, Panel>; + readonly leftFixedArea: Area<IPublicTypePanelConfig, Panel>; - readonly leftFloatArea: Area<PanelConfig, Panel>; + readonly leftFloatArea: Area<IPublicTypePanelConfig, Panel>; - readonly rightArea: Area<PanelConfig, Panel>; + readonly rightArea: Area<IPublicTypePanelConfig, Panel>; - readonly mainArea: Area<WidgetConfig | PanelConfig, Widget | Panel>; + @obx readonly mainArea: Area<WidgetConfig | IPublicTypePanelConfig, Widget | Panel>; - readonly bottomArea: Area<PanelConfig, Panel>; + readonly bottomArea: Area<IPublicTypePanelConfig, Panel>; readonly stages: Area<StageConfig, Stage>; - constructor(readonly editor: Editor) { + readonly widgets: IWidget[] = []; + + readonly focusTracker = new FocusTracker(); + + constructor(readonly editor: IEditor, readonly viewName: string = 'global') { makeObservable(this); this.leftArea = new Area( this, @@ -83,6 +164,17 @@ export class Skeleton { }, false, ); + this.subTopArea = new Area( + this, + 'subTopArea', + (config) => { + if (isWidget(config)) { + return config; + } + return this.createWidget(config); + }, + false, + ); this.toolbar = new Area( this, 'toolbar', @@ -160,7 +252,9 @@ export class Skeleton { this.setupPlugins(); this.setupEvents(); + this.focusTracker.mount(window); } + /** * setup events * @@ -168,11 +262,11 @@ export class Skeleton { */ setupEvents() { // adjust pinned status when panel shown - this.editor.on('skeleton.panel.show', (panelName, panel) => { + this.editor.eventBus.on(SkeletonEvents.PANEL_SHOW, (panelName, panel) => { const panelNameKey = `${panelName}-pinned-status-isFloat`; - const isInFloatAreaPreferenceExists = this.editor?.getPreference()?.contains(panelNameKey, 'skeleton'); + const isInFloatAreaPreferenceExists = engineConfig.getPreference()?.contains(panelNameKey, 'skeleton'); if (isInFloatAreaPreferenceExists) { - const isInFloatAreaFromPreference = this.editor?.getPreference()?.get(panelNameKey, 'skeleton'); + const isInFloatAreaFromPreference = engineConfig.getPreference()?.get(panelNameKey, 'skeleton'); const isCurrentInFloatArea = panel?.isChildOfFloatArea(); if (isInFloatAreaFromPreference !== isCurrentInFloatArea) { this.toggleFloatStatus(panel); @@ -199,7 +293,7 @@ export class Skeleton { this.leftFloatArea.add(panel); this.leftFloatArea.container.active(panel); } - this.editor?.getPreference()?.set(`${panel.name}-pinned-status-isFloat`, !isFloat, 'skeleton'); + engineConfig.getPreference().set(`${panel.name}-pinned-status-isFloat`, !isFloat, 'skeleton'); } buildFromConfig(config?: EditorConfig, components: PluginClassSet = {}) { @@ -222,8 +316,8 @@ export class Skeleton { Object.keys(plugins).forEach((area) => { plugins[area].forEach((item) => { const { pluginKey, type, props = {}, pluginProps } = item; - const config: Partial<IWidgetBaseConfig> = { - area: area as IWidgetConfigArea, + const config: IPublicTypeWidgetBaseConfig = { + area: area as IPublicTypeWidgetConfigArea, type: 'Widget', name: pluginKey, contentProps: pluginProps, @@ -250,18 +344,16 @@ export class Skeleton { if (pluginKey in components) { config.content = components[pluginKey]; } - this.add(config as IWidgetBaseConfig); + this.add(config); }); }); } postEvent(event: SkeletonEvents, ...args: any[]) { - this.editor.emit(event, ...args); + this.editor.eventBus.emit(event, ...args); } - readonly widgets: IWidget[] = []; - - createWidget(config: IWidgetBaseConfig | IWidget) { + createWidget(config: IPublicTypeWidgetBaseConfig | IWidget) { if (isWidget(config)) { return config; } @@ -296,10 +388,11 @@ export class Skeleton { return this.widgets.find(widget => widget.name === name); } - createPanel(config: PanelConfig) { + createPanel(config: IPublicTypePanelConfig) { const parsedConfig = this.parseConfig(config); - const panel = new Panel(this, parsedConfig as PanelConfig); + const panel = new Panel(this, parsedConfig as IPublicTypePanelConfig); this.panels.set(panel.name, panel); + logger.debug(`Panel created with name: ${panel.name} \nconfig:`, config, '\n current panels: ', this.panels); return panel; } @@ -332,7 +425,7 @@ export class Skeleton { return container; } - private parseConfig(config: IWidgetBaseConfig) { + private parseConfig(config: IPublicTypeWidgetBaseConfig) { if (config.parsed) { return config; } @@ -358,11 +451,35 @@ export class Skeleton { return restConfig; } - add(config: IWidgetBaseConfig, extraConfig?: Record<string, any>) { - const parsedConfig = { + registerConfigTransducer( + transducer: IPublicTypeConfigTransducer, + level = 100, + id?: string, + ) { + transducer.level = level; + transducer.id = id; + const i = this.configTransducers.findIndex((item) => item.level != null && item.level > level); + if (i < 0) { + this.configTransducers.push(transducer); + } else { + this.configTransducers.splice(i, 0, transducer); + } + } + + getRegisteredConfigTransducers(): IPublicTypeConfigTransducer[] { + return this.configTransducers; + } + + add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IWidget | Widget | Panel | Stage | Dock | PanelDock | undefined { + const registeredTransducers = this.getRegisteredConfigTransducers(); + + const parsedConfig = registeredTransducers.reduce((prevConfig, current) => { + return current(prevConfig); + }, { ...this.parseConfig(config), ...extraConfig, - }; + }); + let { area } = parsedConfig; if (!area) { if (parsedConfig.type === 'Panel') { @@ -379,24 +496,26 @@ export class Skeleton { return this.leftArea.add(parsedConfig as PanelDockConfig); case 'rightArea': case 'right': - return this.rightArea.add(parsedConfig as PanelConfig); + return this.rightArea.add(parsedConfig as IPublicTypePanelConfig); case 'topArea': case 'top': return this.topArea.add(parsedConfig as PanelDockConfig); + case 'subTopArea': + return this.subTopArea.add(parsedConfig as PanelDockConfig); case 'toolbar': return this.toolbar.add(parsedConfig as PanelDockConfig); case 'mainArea': case 'main': case 'center': case 'centerArea': - return this.mainArea.add(parsedConfig as PanelConfig); + return this.mainArea.add(parsedConfig as IPublicTypePanelConfig); case 'bottomArea': case 'bottom': - return this.bottomArea.add(parsedConfig as PanelConfig); + return this.bottomArea.add(parsedConfig as IPublicTypePanelConfig); case 'leftFixedArea': - return this.leftFixedArea.add(parsedConfig as PanelConfig); + return this.leftFixedArea.add(parsedConfig as IPublicTypePanelConfig); case 'leftFloatArea': - return this.leftFloatArea.add(parsedConfig as PanelConfig); + return this.leftFloatArea.add(parsedConfig as IPublicTypePanelConfig); case 'stages': return this.stages.add(parsedConfig as StageConfig); default: diff --git a/packages/editor-skeleton/src/transducers/addon-combine.ts b/packages/editor-skeleton/src/transducers/addon-combine.ts index 8975e7efa0..c2bc2dd4c5 100644 --- a/packages/editor-skeleton/src/transducers/addon-combine.ts +++ b/packages/editor-skeleton/src/transducers/addon-combine.ts @@ -1,8 +1,14 @@ -import { TransformedComponentMetadata, FieldConfig, SettingTarget } from '@alilc/lowcode-types'; +import { + IPublicTypeTransformedComponentMetadata, + IPublicTypeFieldConfig, + IPublicModelSettingField, +} from '@alilc/lowcode-types'; import { IconSlot } from '../icons/slot'; import { getConvertedExtraKey } from '@alilc/lowcode-designer'; -export default function (metadata: TransformedComponentMetadata): TransformedComponentMetadata { +export default function ( + metadata: IPublicTypeTransformedComponentMetadata, +): IPublicTypeTransformedComponentMetadata { const { componentName, configure = {} } = metadata; // 如果已经处理过,不再重新执行一遍 @@ -111,37 +117,35 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom }, ]; } - /* - propsGroup.push({ - name: '#generals', - title: { type: 'i18n', 'zh-CN': '通用', 'en-US': 'General' }, - items: [ - { - name: 'id', - title: 'ID', - setter: 'StringSetter', - }, - { - name: 'key', - title: 'Key', - // todo: use Mixin - setter: 'StringSetter', - }, - { - name: 'ref', - title: 'Ref', - setter: 'StringSetter', - }, - { - name: '!more', - title: '更多', - setter: 'PropertiesSetter', - }, - ], - }); - */ - const stylesGroup: FieldConfig[] = []; - const advancedGroup: FieldConfig[] = []; + // propsGroup.push({ + // name: '#generals', + // title: { type: 'i18n', 'zh-CN': '通用', 'en-US': 'General' }, + // items: [ + // { + // name: 'id', + // title: 'ID', + // setter: 'StringSetter', + // }, + // { + // name: 'key', + // title: 'Key', + // // todo: use Mixin + // setter: 'StringSetter', + // }, + // { + // name: 'ref', + // title: 'Ref', + // setter: 'StringSetter', + // }, + // { + // name: '!more', + // title: '更多', + // setter: 'PropertiesSetter', + // }, + // ], + // }); + const stylesGroup: IPublicTypeFieldConfig[] = []; + const advancedGroup: IPublicTypeFieldConfig[] = []; if (propsGroup) { let l = propsGroup.length; while (l-- > 0) { @@ -164,7 +168,7 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom } } } - const combined: FieldConfig[] = [ + const combined: IPublicTypeFieldConfig[] = [ { title: { type: 'i18n', 'zh-CN': '属性', 'en-US': 'Props' }, name: '#props', @@ -210,24 +214,30 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom definition: eventsDefinition, }, }, - getValue(field: SettingTarget, val?: any[]) { + getValue(field: IPublicModelSettingField, val?: any[]) { return val; }, - setValue(field: SettingTarget, eventData) { + setValue(field: IPublicModelSettingField, eventData) { const { eventDataList, eventList } = eventData; - Array.isArray(eventList) && eventList.map((item) => { - field.parent.clearPropValue(item.name); - return item; - }); - Array.isArray(eventDataList) && eventDataList.map((item) => { - field.parent.setPropValue(item.name, { - type: 'JSFunction', - // 需要传下入参 - value: `function(){this.${item.relatedEventName}.apply(this,Array.prototype.slice.call(arguments).concat([${item.paramStr ? item.paramStr : ''}])) }`, + Array.isArray(eventList) && + eventList.map((item) => { + field.parent.clearPropValue(item.name); + return item; + }); + Array.isArray(eventDataList) && + eventDataList.map((item) => { + field.parent.setPropValue(item.name, { + type: 'JSFunction', + // 需要传下入参 + value: `function(){return this.${ + item.relatedEventName + }.apply(this,Array.prototype.slice.call(arguments).concat([${ + item.paramStr ? item.paramStr : '' + }])) }`, + }); + return item; }); - return item; - }); }, }, ], @@ -296,7 +306,7 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom }, { name: 'key', - title: '循环 Key', + title: { type: 'i18n', 'zh-CN': '循环 Key', 'en-US': 'Loop Key' }, setter: [ { componentName: 'StringSetter', @@ -317,9 +327,17 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom advancedGroup.push({ name: 'key', title: { - label: '渲染唯一标识(key)', - tip: '搭配「条件渲染」或「循环渲染」时使用,和 react 组件中的 key 原理相同,点击查看帮助', - docUrl: 'https://lowcode-engine.cn/docV2/qm75w3', + label: { + type: 'i18n', + 'zh-CN': '渲染唯一标识 (key)', + 'en-US': 'Render unique identifier (key)', + }, + tip: { + type: 'i18n', + 'zh-CN': '搭配「条件渲染」或「循环渲染」时使用,和 react 组件中的 key 原理相同,点击查看帮助', + 'en-US': 'Used with 「Conditional Rendering」or「Cycle Rendering」, the same principle as the key in the react component, click to view the help', + }, + docUrl: 'https://www.yuque.com/lce/doc/qm75w3', }, setter: [ { diff --git a/packages/editor-skeleton/src/transducers/parse-func.ts b/packages/editor-skeleton/src/transducers/parse-func.ts index 8dc0a4cd84..bc52deec35 100644 --- a/packages/editor-skeleton/src/transducers/parse-func.ts +++ b/packages/editor-skeleton/src/transducers/parse-func.ts @@ -1,12 +1,10 @@ -import { - FieldConfig, - TransformedComponentMetadata, - isJSFunction, -} from '@alilc/lowcode-types'; -import { isPlainObject } from '@alilc/lowcode-utils'; +import { IPublicTypeTransformedComponentMetadata } from '@alilc/lowcode-types'; +import { isPlainObject, isJSFunction, getLogger } from '@alilc/lowcode-utils'; const leadingFnRe = /^function/; const leadingFnNameRe = /^\w+\s*\(/; +const logger = getLogger({ level: 'warn', bizName: 'skeleton:transducers' }); + /** * 将函数字符串转成函数,支持几种类型 * 类型一:() => {} / val => {} @@ -28,7 +26,7 @@ function transformStringToFunction(str: string) { try { return (${str}).apply(self, arguments); } catch(e) { - console.log('call function which parsed by lowcode failed: ', e); + console.warn('call function which parsed by lowcode failed: ', e); return e.message; } }; @@ -37,8 +35,8 @@ function transformStringToFunction(str: string) { // eslint-disable-next-line no-new-func fn = new Function(fnBody)(); } catch (e) { - console.error(str); - console.error(e.message); + logger.error(str); + logger.error(e.message); } return fn; } @@ -57,7 +55,7 @@ function parseJSFunc(obj: any, enableAllowedKeys = true) { }); } -export default function (metadata: TransformedComponentMetadata): TransformedComponentMetadata { +export default function (metadata: IPublicTypeTransformedComponentMetadata): IPublicTypeTransformedComponentMetadata { parseJSFunc(metadata, false); return metadata; diff --git a/packages/editor-skeleton/src/transducers/parse-props.ts b/packages/editor-skeleton/src/transducers/parse-props.ts index cbecab5d60..573d24ac62 100644 --- a/packages/editor-skeleton/src/transducers/parse-props.ts +++ b/packages/editor-skeleton/src/transducers/parse-props.ts @@ -1,17 +1,18 @@ import { - FieldConfig, - PropConfig, - PropType, - SetterType, - OneOf, - ObjectOf, - ArrayOf, - TransformedComponentMetadata, - OneOfType, + IPublicTypeFieldConfig, + IPublicTypePropConfig, + IPublicTypePropType, + IPublicTypeSetterType, + IPublicTypeOneOf, + IPublicTypeObjectOf, + IPublicTypeArrayOf, + IPublicTypeTransformedComponentMetadata, + IPublicTypeOneOfType, ConfigureSupportEvent, + IPublicModelSettingField, } from '@alilc/lowcode-types'; -function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig { +function propConfigToFieldConfig(propConfig: IPublicTypePropConfig): IPublicTypeFieldConfig { const { name, description } = propConfig; const title = { label: { @@ -29,7 +30,7 @@ function propConfigToFieldConfig(propConfig: PropConfig): FieldConfig { }; } -function propTypeToSetter(propType: PropType): SetterType { +function propTypeToSetter(propType: IPublicTypePropType): IPublicTypeSetterType { let typeName: string; let isRequired: boolean | undefined = false; if (typeof propType === 'string') { @@ -61,7 +62,7 @@ function propTypeToSetter(propType: PropType): SetterType { initialValue: false, }; case 'oneOf': - const dataSource = ((propType as OneOf).value || []).map((value, index) => { + const dataSource = ((propType as IPublicTypeOneOf).value || []).map((value, index) => { const t = typeof value; return { label: t === 'string' || t === 'number' || t === 'boolean' ? String(value) : `value ${index}`, @@ -102,7 +103,7 @@ function propTypeToSetter(propType: PropType): SetterType { }, }, isRequired, - initialValue: (field: any) => { + initialValue: (field: IPublicModelSettingField) => { const data: any = {}; items.forEach((item: any) => { let initial = item.defaultValue; @@ -120,7 +121,7 @@ function propTypeToSetter(propType: PropType): SetterType { componentName: 'ObjectSetter', props: { config: { - extraSetter: propTypeToSetter(typeName === 'objectOf' ? (propType as ObjectOf).value : 'any'), + extraSetter: propTypeToSetter(typeName === 'objectOf' ? (propType as IPublicTypeObjectOf).value : 'any'), }, }, isRequired, @@ -131,7 +132,7 @@ function propTypeToSetter(propType: PropType): SetterType { return { componentName: 'ArraySetter', props: { - itemSetter: propTypeToSetter(typeName === 'arrayOf' ? (propType as ArrayOf).value : 'any'), + itemSetter: propTypeToSetter(typeName === 'arrayOf' ? (propType as IPublicTypeArrayOf).value : 'any'), }, isRequired, initialValue: [], @@ -151,7 +152,7 @@ function propTypeToSetter(propType: PropType): SetterType { componentName: 'MixedSetter', props: { // TODO: - setters: (propType as OneOfType).value.map((item) => propTypeToSetter(item)), + setters: (propType as IPublicTypeOneOfType).value.map((item) => propTypeToSetter(item)), }, isRequired, }; @@ -167,7 +168,7 @@ function propTypeToSetter(propType: PropType): SetterType { const EVENT_RE = /^on|after|before[A-Z][\w]*$/; -export default function (metadata: TransformedComponentMetadata): TransformedComponentMetadata { +export default function (metadata: IPublicTypeTransformedComponentMetadata): IPublicTypeTransformedComponentMetadata { const { configure = {} } = metadata; // TODO types后续补充 let extendsProps: any = null; @@ -205,7 +206,7 @@ export default function (metadata: TransformedComponentMetadata): TransformedCom } const { component = {}, supports = {} } = configure; const supportedEvents: ConfigureSupportEvent[] | null = supports.events ? null : []; - const props: FieldConfig[] = []; + const props: IPublicTypeFieldConfig[] = []; metadata.props.forEach((prop) => { const { name, propType, description } = prop; diff --git a/packages/editor-skeleton/src/types.ts b/packages/editor-skeleton/src/types.ts index cefe86c927..8c5f1484a0 100644 --- a/packages/editor-skeleton/src/types.ts +++ b/packages/editor-skeleton/src/types.ts @@ -1,41 +1,20 @@ import { ReactElement, ComponentType } from 'react'; -import { TitleContent, IconType, I18nData, TipContent } from '@alilc/lowcode-types'; +import { + IPublicTypeTitleContent, + IPublicTypeWidgetConfigArea, + IPublicTypeWidgetBaseConfig, + IPublicTypePanelDockProps, + IPublicTypePanelConfigProps, + IPublicTypePanelConfig, +} from '@alilc/lowcode-types'; import { IWidget } from './widget/widget'; -/** - * 所有可能的停靠位置 - */ -export type IWidgetConfigArea = - | 'leftArea' | 'left' | 'rightArea' - | 'right' | 'topArea' | 'top' - | 'toolbar' | 'mainArea' | 'main' - | 'center' | 'centerArea' | 'bottomArea' - | 'bottom' | 'leftFixedArea' - | 'leftFloatArea' | 'stages'; - -export interface IWidgetBaseConfig { - type: string; - name: string; - /** - * 停靠位置: - * - 当 type 为 'Panel' 时自动为 'leftFloatArea'; - * - 当 type 为 'Widget' 时自动为 'mainArea'; - * - 其他时候自动为 'leftArea'; - */ - area?: IWidgetConfigArea; - props?: Record<string, any>; - content?: any; - contentProps?: Record<string, any>; - // index?: number; - [extra: string]: any; -} - -export interface WidgetConfig extends IWidgetBaseConfig { +export interface WidgetConfig extends IPublicTypeWidgetBaseConfig { type: 'Widget'; props?: { align?: 'left' | 'right' | 'bottom' | 'center' | 'top'; onInit?: (widget: IWidget) => void; - title?: TitleContent; + title?: IPublicTypeTitleContent | null; }; content?: string | ReactElement | ComponentType<any>; // children } @@ -44,16 +23,10 @@ export function isWidgetConfig(obj: any): obj is WidgetConfig { return obj && obj.type === 'Widget'; } -export interface DockProps { - title?: TitleContent; - icon?: IconType; - size?: 'small' | 'medium' | 'large'; - className?: string; - description?: TipContent; - onClick?: () => void; +export interface DockProps extends IPublicTypePanelDockProps { } -export interface DividerConfig extends IWidgetBaseConfig { +export interface DividerConfig extends IPublicTypeWidgetBaseConfig { type: 'Divider'; props?: { align?: 'left' | 'right' | 'center'; @@ -64,7 +37,7 @@ export function isDividerConfig(obj: any): obj is DividerConfig { return obj && obj.type === 'Divider'; } -export interface IDockBaseConfig extends IWidgetBaseConfig { +export interface IDockBaseConfig extends IPublicTypeWidgetBaseConfig { props?: DockProps & { align?: 'left' | 'right' | 'bottom' | 'center' | 'top'; onInit?: (widget: IWidget) => void; @@ -84,8 +57,8 @@ export function isDockConfig(obj: any): obj is DockConfig { export interface DialogDockConfig extends IDockBaseConfig { type: 'DialogDock'; dialogProps?: { - title?: TitleContent; [key: string]: any; + title?: IPublicTypeTitleContent; }; } @@ -93,44 +66,17 @@ export function isDialogDockConfig(obj: any): obj is DialogDockConfig { return obj && obj.type === 'DialogDock'; } -// 窗格扩展 -export interface PanelConfig extends IWidgetBaseConfig { - type: 'Panel'; - content?: string | ReactElement | ComponentType<any> | PanelConfig[]; // as children - props?: PanelProps; -} - -export function isPanelConfig(obj: any): obj is PanelConfig { +export function isPanelConfig(obj: any): obj is IPublicTypePanelConfig { return obj && obj.type === 'Panel'; } -export type HelpTipConfig = string | { url?: string; content?: string | ReactElement }; - -export interface PanelProps { - title?: TitleContent; - icon?: any; // 冗余字段 - description?: string | I18nData; - hideTitleBar?: boolean; // panel.props 兼容,不暴露 - help?: HelpTipConfig; // 显示问号帮助 - width?: number; // panel.props - height?: number; // panel.props - maxWidth?: number; // panel.props - maxHeight?: number; // panel.props - condition?: (widget: IWidget) => any; - onInit?: (widget: IWidget) => any; - onDestroy?: () => any; - shortcut?: string; // 只有在特定位置,可触发 toggle show - enableDrag?: boolean; // 是否开启通过 drag 调整 宽度 - keepVisibleWhileDragging?: boolean; // 是否在该 panel 范围内拖拽时保持 visible 状态 -} - export interface PanelDockConfig extends IDockBaseConfig { type: 'PanelDock'; panelName?: string; - panelProps?: PanelProps & { - area?: IWidgetConfigArea; + panelProps?: IPublicTypePanelConfigProps & { + area?: IPublicTypeWidgetConfigArea; }; - content?: string | ReactElement | ComponentType<any> | PanelConfig[]; // content for pane + content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[]; // content for pane } export function isPanelDockConfig(obj: any): obj is PanelDockConfig { diff --git a/packages/editor-skeleton/src/widget/dock.ts b/packages/editor-skeleton/src/widget/dock.ts index e6c8b25ab1..20cdd425da 100644 --- a/packages/editor-skeleton/src/widget/dock.ts +++ b/packages/editor-skeleton/src/widget/dock.ts @@ -3,14 +3,14 @@ import { makeObservable, obx } from '@alilc/lowcode-editor-core'; import { uniqueId, createContent } from '@alilc/lowcode-utils'; import { getEvent } from '@alilc/lowcode-shell'; import { DockConfig } from '../types'; -import { Skeleton } from '../skeleton'; +import { ISkeleton } from '../skeleton'; import { DockView, WidgetView } from '../components/widget-views'; import { IWidget } from './widget'; /** * 带图标(主要)/标题(次要)的扩展 */ -export default class Dock implements IWidget { +export class Dock implements IWidget { readonly isWidget = true; readonly id = uniqueId('dock'); @@ -59,7 +59,7 @@ export default class Dock implements IWidget { return this._body; } - constructor(readonly skeleton: Skeleton, readonly config: DockConfig) { + constructor(readonly skeleton: ISkeleton, readonly config: DockConfig) { makeObservable(this); const { props = {}, name } = config; this.name = name; diff --git a/packages/editor-skeleton/src/widget/index.ts b/packages/editor-skeleton/src/widget/index.ts new file mode 100644 index 0000000000..c6dd495f1b --- /dev/null +++ b/packages/editor-skeleton/src/widget/index.ts @@ -0,0 +1,6 @@ +export * from './widget-container'; +export * from './panel'; +export * from './panel-dock'; +export * from './dock'; +export * from './widget'; +export * from './stage'; \ No newline at end of file diff --git a/packages/editor-skeleton/src/widget/panel-dock.ts b/packages/editor-skeleton/src/widget/panel-dock.ts index 8f3c8b4034..896849706a 100644 --- a/packages/editor-skeleton/src/widget/panel-dock.ts +++ b/packages/editor-skeleton/src/widget/panel-dock.ts @@ -1,15 +1,15 @@ import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core'; import { uniqueId } from '@alilc/lowcode-utils'; import { createElement, ReactNode, ReactInstance } from 'react'; -import { Skeleton } from '../skeleton'; +import { ISkeleton } from '../skeleton'; import { PanelDockConfig } from '../types'; -import Panel from './panel'; +import { Panel } from './panel'; import { PanelDockView, WidgetView } from '../components/widget-views'; import { IWidget } from './widget'; import { composeTitle } from './utils'; import { findDOMNode } from 'react-dom'; -export default class PanelDock implements IWidget { +export class PanelDock implements IWidget { readonly isWidget = true; readonly isPanelDock = true; @@ -18,7 +18,7 @@ export default class PanelDock implements IWidget { readonly name: string; - readonly align?: string; + readonly align?: 'left' | 'right' | 'bottom' | 'center' | 'top' | undefined; private inited = false; @@ -51,11 +51,6 @@ export default class PanelDock implements IWidget { }); } - getDOMNode() { - // eslint-disable-next-line react/no-find-dom-node - return this._shell ? findDOMNode(this._shell) : null; - } - @obx.ref private _visible = true; get visible() { @@ -76,7 +71,7 @@ export default class PanelDock implements IWidget { return this._panel || this.skeleton.getPanel(this.panelName); } - constructor(readonly skeleton: Skeleton, readonly config: PanelDockConfig) { + constructor(readonly skeleton: ISkeleton, readonly config: PanelDockConfig) { makeObservable(this); const { content, contentProps, panelProps, name, props } = config; this.name = name; @@ -84,7 +79,7 @@ export default class PanelDock implements IWidget { this.panelName = config.panelName || name; this.align = props?.align; if (content) { - const _panelProps: any = { ...panelProps }; + const _panelProps = { ...panelProps }; if (_panelProps.title == null && props) { _panelProps.title = composeTitle(props.title, undefined, props.description, true, true); } @@ -102,6 +97,11 @@ export default class PanelDock implements IWidget { } } + getDOMNode() { + // eslint-disable-next-line react/no-find-dom-node + return this._shell ? findDOMNode(this._shell) : null; + } + setVisible(flag: boolean) { if (flag === this._visible) { return; @@ -170,7 +170,6 @@ export default class PanelDock implements IWidget { } } - export function isPanelDock(obj: any): obj is PanelDock { return obj && obj.isPanelDock; } diff --git a/packages/editor-skeleton/src/widget/panel.ts b/packages/editor-skeleton/src/widget/panel.ts index 6c05ee2bd5..3a1ce3b00b 100644 --- a/packages/editor-skeleton/src/widget/panel.ts +++ b/packages/editor-skeleton/src/widget/panel.ts @@ -1,18 +1,16 @@ -import { EventEmitter } from 'events'; import { createElement, ReactNode } from 'react'; -import { obx, computed, makeObservable } from '@alilc/lowcode-editor-core'; +import { obx, computed, makeObservable, IEventBus, createModuleEventBus } from '@alilc/lowcode-editor-core'; import { uniqueId, createContent } from '@alilc/lowcode-utils'; -import { TitleContent } from '@alilc/lowcode-types'; -import WidgetContainer from './widget-container'; +import { IPublicTypeHelpTipConfig, IPublicTypePanelConfig, IPublicTypeTitleContent } from '@alilc/lowcode-types'; +import { WidgetContainer } from './widget-container'; import { getEvent } from '@alilc/lowcode-shell'; -import { PanelConfig, HelpTipConfig } from '../types'; import { TitledPanelView, TabsPanelView, PanelView } from '../components/widget-views'; -import { Skeleton } from '../skeleton'; +import { ISkeleton } from '../skeleton'; import { composeTitle } from './utils'; import { IWidget } from './widget'; -import PanelDock, { isPanelDock } from './panel-dock'; +import { isPanelDock, PanelDock } from './panel-dock'; -export default class Panel implements IWidget { +export class Panel implements IWidget { readonly isWidget = true; readonly name: string; @@ -23,7 +21,7 @@ export default class Panel implements IWidget { @obx.ref private _actived = false; - private emitter = new EventEmitter(); + private emitter: IEventBus = createModuleEventBus('Panel'); @computed get actived(): boolean { return this._actived; @@ -46,6 +44,7 @@ export default class Panel implements IWidget { if (this.container) { return createElement(TabsPanelView, { container: this.container, + shouldHideSingleTab: true, }); } @@ -71,17 +70,17 @@ export default class Panel implements IWidget { return createElement(TitledPanelView, { panel: this, key: this.id, area }); } - readonly title: TitleContent; + readonly title: IPublicTypeTitleContent; - readonly help?: HelpTipConfig; + readonly help?: IPublicTypeHelpTipConfig; private plain = false; - private container?: WidgetContainer<Panel, PanelConfig>; + private container?: WidgetContainer<Panel, IPublicTypePanelConfig>; @obx.ref public parent?: WidgetContainer; - constructor(readonly skeleton: Skeleton, readonly config: PanelConfig) { + constructor(readonly skeleton: ISkeleton, readonly config: IPublicTypePanelConfig) { makeObservable(this); const { name, content, props = {} } = config; const { hideTitleBar, title, icon, description, help } = props; @@ -91,9 +90,6 @@ export default class Panel implements IWidget { this.plain = hideTitleBar || !title; this.help = help; if (Array.isArray(content)) { - if (content.length === 1) { - // todo: not show tabs - } this.container = this.skeleton.createContainer( name, (item) => { @@ -112,7 +108,7 @@ export default class Panel implements IWidget { props.onInit.call(this, this); } - if (content.onInit) { + if (typeof content !== 'string' && content && content.onInit) { content.onInit.call(this, this); } // todo: process shortcut @@ -128,7 +124,7 @@ export default class Panel implements IWidget { this.parent = parent; } - add(item: Panel | PanelConfig) { + add(item: Panel | IPublicTypePanelConfig) { return this.container?.add(item); } @@ -212,6 +208,10 @@ export default class Panel implements IWidget { this.setActive(false); } + disable() {} + + enable(): void {} + show() { this.setActive(true); } diff --git a/packages/editor-skeleton/src/widget/stage.ts b/packages/editor-skeleton/src/widget/stage.ts index adfebdc9d6..2b177af61f 100644 --- a/packages/editor-skeleton/src/widget/stage.ts +++ b/packages/editor-skeleton/src/widget/stage.ts @@ -1,6 +1,6 @@ // import { uniqueId } from '@alilc/lowcode-utils'; -import Widget from './widget'; -import { Skeleton } from '../skeleton'; +import { Widget } from './widget'; +import { ISkeleton } from '../skeleton'; import { WidgetConfig } from '../types'; export interface StageConfig extends WidgetConfig { @@ -17,7 +17,7 @@ export class Stage extends Widget { direction?: 'right' | 'left'; }; - constructor(skeleton: Skeleton, config: StageConfig) { + constructor(skeleton: ISkeleton, config: StageConfig) { super(skeleton, config); this.isRoot = config.isRoot || false; } diff --git a/packages/editor-skeleton/src/widget/utils.ts b/packages/editor-skeleton/src/widget/utils.ts index 73657d08f7..bec41333c8 100644 --- a/packages/editor-skeleton/src/widget/utils.ts +++ b/packages/editor-skeleton/src/widget/utils.ts @@ -1,46 +1,53 @@ -import { IconType, TitleContent, isI18nData, TipContent, isTitleConfig } from '@alilc/lowcode-types'; +import { IPublicTypeIconType, IPublicTypeTitleContent, TipContent } from '@alilc/lowcode-types'; +import { isI18nData, isTitleConfig } from '@alilc/lowcode-utils'; import { isValidElement } from 'react'; -export function composeTitle(title?: TitleContent, icon?: IconType, tip?: TipContent, tipAsTitle?: boolean, noIcon?: boolean) { +export function composeTitle(title?: IPublicTypeTitleContent, icon?: IPublicTypeIconType, tip?: TipContent, tipAsTitle?: boolean, noIcon?: boolean) { + let _title: IPublicTypeTitleContent | undefined; if (!title) { - title = {}; + _title = {}; if (!icon || tipAsTitle) { - title.label = tip; + _title = { + label: tip, + }; tip = undefined; } + } else { + _title = title; } + if (icon || tip) { - if (typeof title !== 'object' || isValidElement(title) || isI18nData(title)) { - if (isValidElement(title)) { - if (title.type === 'svg' || (title.type as any).getIcon) { + if (typeof _title !== 'object' || isValidElement(_title) || isI18nData(_title)) { + if (isValidElement(_title)) { + if (_title.type === 'svg' || _title.type.getIcon) { if (!icon) { - icon = title as any; + icon = _title; } if (tipAsTitle) { - title = tip as any; + _title = tip; tip = null; } else { - title = undefined; + _title = undefined; } } } - title = { - label: title, + _title = { + label: _title, icon, tip, }; } else { - title = { - ...title, + _title = { + ..._title, icon, tip, }; } } - if (isTitleConfig(title) && noIcon) { - if (!isValidElement(title)) { - title.icon = undefined; + if (isTitleConfig(_title) && noIcon) { + if (!isValidElement(_title)) { + _title.icon = undefined; } } - return title; + return _title; } diff --git a/packages/editor-skeleton/src/widget/widget-container.ts b/packages/editor-skeleton/src/widget/widget-container.ts index 2774cc0fbc..183ff8fe09 100644 --- a/packages/editor-skeleton/src/widget/widget-container.ts +++ b/packages/editor-skeleton/src/widget/widget-container.ts @@ -14,7 +14,7 @@ function isActiveable(obj: any): obj is Activeable { return obj && obj.setActive; } -export default class WidgetContainer<T extends WidgetItem = any, G extends WidgetItem = any> { +export class WidgetContainer<T extends WidgetItem = any, G extends WidgetItem = any> { @obx.shallow items: T[] = []; private maps: { [name: string]: T } = {}; @@ -81,7 +81,7 @@ export default class WidgetContainer<T extends WidgetItem = any, G extends Widge } unactiveAll() { - Object.keys(this.maps).forEach(name => this.unactive(name)); + Object.keys(this.maps).forEach((name) => this.unactive(name)); } add(item: T | G): T { @@ -101,7 +101,8 @@ export default class WidgetContainer<T extends WidgetItem = any, G extends Widge item.setParent(this); } if (this.defaultSetCurrent) { - if (!this._current) { + const shouldHiddenWhenInit = (item as any).config?.props?.hiddenWhenInit; + if (!this._current && !shouldHiddenWhenInit) { this.active(item); } } diff --git a/packages/editor-skeleton/src/widget/widget.ts b/packages/editor-skeleton/src/widget/widget.ts index 9934af846b..c956738774 100644 --- a/packages/editor-skeleton/src/widget/widget.ts +++ b/packages/editor-skeleton/src/widget/widget.ts @@ -2,10 +2,10 @@ import { ReactNode, createElement } from 'react'; import { makeObservable, obx } from '@alilc/lowcode-editor-core'; import { createContent, uniqueId } from '@alilc/lowcode-utils'; import { getEvent } from '@alilc/lowcode-shell'; -import { WidgetConfig, IWidgetBaseConfig } from '../types'; -import { Skeleton } from '../skeleton'; +import { WidgetConfig } from '../types'; +import { ISkeleton } from '../skeleton'; import { WidgetView } from '../components/widget-views'; -import { TitleContent } from '@alilc/lowcode-types'; +import { IPublicTypeTitleContent, IPublicTypeWidgetBaseConfig } from '@alilc/lowcode-types'; export interface IWidget { readonly name: string; @@ -15,8 +15,8 @@ export interface IWidget { readonly visible: boolean; readonly disabled?: boolean; readonly body: ReactNode; - readonly skeleton: Skeleton; - readonly config: IWidgetBaseConfig; + readonly skeleton: ISkeleton; + readonly config: IPublicTypeWidgetBaseConfig; getName(): string; getContent(): any; @@ -27,7 +27,7 @@ export interface IWidget { disable?(): void; } -export default class Widget implements IWidget { +export class Widget implements IWidget { readonly isWidget = true; readonly id = uniqueId('widget'); @@ -69,9 +69,9 @@ export default class Widget implements IWidget { }); } - readonly title: TitleContent; + readonly title: IPublicTypeTitleContent; - constructor(readonly skeleton: Skeleton, readonly config: WidgetConfig) { + constructor(readonly skeleton: ISkeleton, readonly config: WidgetConfig) { makeObservable(this); const { props = {}, name } = config; this.name = name; @@ -138,4 +138,3 @@ export default class Widget implements IWidget { export function isWidget(obj: any): obj is IWidget { return obj && obj.isWidget; } - diff --git a/packages/editor-skeleton/tests/widget/utils.test.ts b/packages/editor-skeleton/tests/widget/utils.test.ts new file mode 100644 index 0000000000..2836e86db5 --- /dev/null +++ b/packages/editor-skeleton/tests/widget/utils.test.ts @@ -0,0 +1,54 @@ +import { composeTitle } from '../../src/widget/utils'; +import * as React from 'react'; + +const label = React.createElement('div'); + +describe('composeTitle 测试', () => { + it('基础能力测试', () => { + expect(composeTitle(undefined)).toEqual({ + label: undefined, + }); + + expect(composeTitle(undefined, undefined, 'tips', true, true)).toEqual({ + icon: undefined, + label: 'tips', + }); + + expect(composeTitle(undefined, undefined, label, true, true)).toEqual({ + icon: undefined, + label, + }); + + expect(composeTitle({ + icon: undefined, + label, + }, undefined, '')).toEqual({ + icon: undefined, + label, + }); + + expect(composeTitle('settingsPane')).toEqual('settingsPane'); + + expect(composeTitle(label, undefined, '物料面板', true, true)).toEqual({ + icon: undefined, + label, + tip: '物料面板', + }); + + expect(composeTitle(label, undefined, label, true, true)).toEqual({ + icon: undefined, + label, + tip: label, + }); + + expect(composeTitle({ + label: "物料面板", + icon: undefined, + tip: null, + })).toEqual({ + label: "物料面板", + icon: undefined, + tip: null, + }) + }); +}) \ No newline at end of file diff --git a/packages/engine/README-zh_CN.md b/packages/engine/README-zh_CN.md index 5a070affb2..5442aa58bd 100644 --- a/packages/engine/README-zh_CN.md +++ b/packages/engine/README-zh_CN.md @@ -14,7 +14,9 @@ [![][issues-helper-image]][issues-helper-url] [![Issues need help][help-wanted-image]][help-wanted-url] -[![codecov][codecov-image-url]][codecov-url] +[![codecov][codecov-image-url]][codecov-url] [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/lowcode-workspace/awesome-lowcode-engine) + +[![](https://img.shields.io/badge/LowCodeEngine-%E6%9F%A5%E7%9C%8B%E8%B4%A1%E7%8C%AE%E6%8E%92%E8%A1%8C%E6%A6%9C-orange)](https://opensource.alibaba.com/contribution_leaderboard/details?projectValue=lowcode-engine) [npm-image]: https://img.shields.io/npm/v/@alilc/lowcode-engine.svg?style=flat-square [npm-url]: http://npmjs.org/package/@alilc/lowcode-engine @@ -41,7 +43,7 @@ - 🌈 提炼自企业级低代码平台的面向扩展设计的内核引擎,奉行最小内核,最强生态的设计理念 - 📦 开箱即用的高质量生态元素,包括 物料体系、设置器、插件 等 - ⚙️ 完善的工具链,支持 物料体系、设置器、插件 等生态元素的全链路研发周期 -- 🔌 强大的扩展能力,已支撑近 100 个各种垂直类低代码平台 +- 🔌 强大的扩展能力,已支撑 100+ 个各种类型低代码平台 - 🛡 使用 TypeScript 开发,提供完整的类型定义文件 ## 🎯 兼容环境 @@ -69,7 +71,7 @@ skeleton.add({ area: 'topArea', type: 'Widget', name: 'logo', - content: YourFantaticLogo, + content: YourFantasticLogo, contentProps: { logo: 'https://img.alicdn.com/tfs/TB1_SocGkT2gK0jSZFkXXcIQFXa-66-66.png', @@ -97,27 +99,34 @@ init(document.getElementById('lce')); ### cdn 可选方式: #### 方式 1(推荐):alifd cdn ```html -https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js + +https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js +``` + +#### 方式 2(推荐):uipaas cdn +```html +https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/1.0.18/dist/js/engine-core.js -https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://uipaas-assets.com/prod/npm/@alilc/lowcode-react-simulator-renderer/1.0.18/dist/js/react-simulator-renderer.js ``` -#### 方式 2:unpkg +#### 方式 3:unpkg ```html -https://unpkg.com/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://unpkg.com/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js -https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js ``` -#### 方式 3:jsdelivr +#### 方式 4:jsdelivr ```html -https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js -https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js ``` -#### 方式 4:使用自有 cdn -将源码中 packages/engine/dist 和 packages/(react|rax)-simulator-renderer/dist 下的文件传至你的 cdn 提供商 +#### 方式 5:使用自有 cdn +将源码中 packages/engine/dist 和 packages/react-simulator-renderer/dist 下的文件传至你的 cdn 提供商 ## 🔗 相关链接 @@ -126,9 +135,9 @@ https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/ - [官方物料](https://github.com/alibaba/lowcode-materials) - [官方设置器(setter)](https://github.com/alibaba/lowcode-engine-ext) - [官方插件(plugin)](https://github.com/alibaba/lowcode-plugins) -- [生态元素(物料、setter、插件)工具链](https://www.yuque.com/lce/doc/ulvlkz) -- [用户文档](https://lowcode-engine.cn/docV2) -- [API](https://lowcode-engine.cn/docV2/vlmeme) +- [生态元素(物料、setter、插件)工具链](https://lowcode-engine.cn/site/docs/guide/expand/editor/cli) +- [用户文档](https://lowcode-engine.cn/doc) +- [API](https://lowcode-engine.cn/site/docs/api/) [awesome-lowcode-engine](https://github.com/lowcode-workspace/awesome-lowcode-engine) 中包含了一系列围绕引擎建设的工具、解决方案等,如果你有类似的解决方案或者工具,欢迎提 PR 到该仓库,让更多人了解到 @@ -146,14 +155,14 @@ $ npm start > > 📢 windows 环境必须使用 [WSL](https://docs.microsoft.com/zh-cn/windows/wsl/install),其他终端不保证能正常运行 -lowcode-engine 启动后,提供了几个 umd 文件,可以结合 [lowcode-demo](https://github.com/alibaba/lowcode-demo) 项目做调试,文件代理规则参考[这里](https://www.yuque.com/lce/doc/glz0fx)。 +lowcode-engine 启动后,提供了几个 umd 文件,可以结合 [lowcode-demo](https://github.com/alibaba/lowcode-demo) 项目做调试,文件代理规则参考[这里](https://lowcode-engine.cn/site/docs/participate/prepare#2-配置资源代理)。 ## 🤝 参与共建 请先阅读: -1. [如何配置引擎调试环境?](https://www.yuque.com/lce/doc/glz0fx) -2. [关于引擎的研发协作流程](https://www.yuque.com/lce/doc/contributing) -3. [引擎的工程化配置](https://www.yuque.com/lce/doc/gxwqg6) +1. [如何配置引擎调试环境?](https://lowcode-engine.cn/site/docs/participate/prepare) +2. [关于引擎的研发协作流程](https://lowcode-engine.cn/site/docs/participate/flow) +3. [引擎的工程化配置](https://lowcode-engine.cn/site/docs/participate/config) > 强烈推荐阅读 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way)、[《如何向开源社区提问题》](https://github.com/seajs/seajs/issues/545) 和 [《如何有效地报告 Bug》](http://www.chiark.greenend.org.uk/%7Esgtatham/bugs-cn.html)、[《如何向开源项目提交无法解答的问题》](https://zhuanlan.zhihu.com/p/25795393),更好的问题更容易获得帮助。(此段参考 [antd](https://github.com/ant-design/ant-design)) @@ -166,4 +175,4 @@ lowcode-engine 启动后,提供了几个 umd 文件,可以结合 [lowcode-de <p> <a href="https://github.com/alibaba/lowcode-engine/graphs/contributors"><img src="https://contrib.rocks/image?repo=alibaba/lowcode-engine" /></a> -</p> \ No newline at end of file +</p> diff --git a/packages/engine/README.md b/packages/engine/README.md index 63e9671403..ae4e7fd43b 100644 --- a/packages/engine/README.md +++ b/packages/engine/README.md @@ -14,7 +14,9 @@ An enterprise-class low-code technology stack with scale-out design [![][issues-helper-image]][issues-helper-url] [![Issues need help][help-wanted-image]][help-wanted-url] -[![codecov][codecov-image-url]][codecov-url] +[![codecov][codecov-image-url]][codecov-url] [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/lowcode-workspace/awesome-lowcode-engine) + +[![](https://img.shields.io/badge/LowCodeEngine-Check%20Your%20Contribution-orange)](https://opensource.alibaba.com/contribution_leaderboard/details?projectValue=lowcode-engine) [npm-image]: https://img.shields.io/npm/v/@alilc/lowcode-engine.svg?style=flat-square [npm-url]: http://npmjs.org/package/@alilc/lowcode-engine @@ -69,7 +71,7 @@ skeleton.add({ area: 'topArea', type: 'Widget', name: 'logo', - content: YourFantaticLogo, + content: YourFantasticLogo, contentProps: { logo: 'https://img.alicdn.com/tfs/TB1_SocGkT2gK0jSZFkXXcIQFXa-66-66.png', @@ -97,27 +99,34 @@ init(document.getElementById('lce')); ### cdn optional method: #### Method 1: alifd cdn ```html -https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://alifd.alicdn.com/npm/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js + +https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js +``` + +#### Method 2: uipaas cdn +```html +https://uipaas-assets.com/prod/npm/@alilc/lowcode-engine/1.0.18/dist/js/engine-core.js -https://alifd.alicdn.com/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://uipaas-assets.com/prod/npm/@alilc/lowcode-react-simulator-renderer/1.0.18/dist/js/react-simulator-renderer.js ``` -#### Method 2: unpkg +#### Method 3: unpkg ```html -https://unpkg.com/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://unpkg.com/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js -https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://unpkg.com/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js ``` -#### Method 3: jsdelivr +#### Method 4: jsdelivr ```html -https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.0/dist/js/engine-core.js +https://cdn.jsdelivr.net/npm/@alilc/lowcode-engine@1.0.18/dist/js/engine-core.js -https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.0/dist/js/react-simulator-renderer.js +https://cdn.jsdelivr.net/npm/@alilc/lowcode-react-simulator-renderer@1.0.18/dist/js/react-simulator-renderer.js ``` -#### Method 4: Use your own cdn -Pass the files under packages/engine/dist and packages/(react|rax)-simulator-renderer/dist in the source code to your cdn provider +#### Method 5: Use your own cdn +Pass the files under packages/engine/dist and packages/react-simulator-renderer/dist in the source code to your cdn provider ## 🔗 Related Links @@ -126,9 +135,9 @@ Pass the files under packages/engine/dist and packages/(react|rax)-simulator-ren - [Official Materials](https://github.com/alibaba/lowcode-materials) - [official setter](https://github.com/alibaba/lowcode-engine-ext) - [Official plugin (plugin)](https://github.com/alibaba/lowcode-plugins) -- [Ecological elements (materials, setters, plugins) toolchain](https://www.yuque.com/lce/doc/ulvlkz) -- [User Documentation](http://lowcode-engine.cn/docV2) -- [API](http://lowcode-engine.cn/docV2/vlmeme) +- [Ecological elements (materials, setters, plugins) toolchain](https://lowcode-engine.cn/site/docs/guide/expand/editor/cli) +- [User Documentation](http://lowcode-engine.cn/doc) +- [API](https://lowcode-engine.cn/site/docs/api/) This [awesome-lowcode-engine](https://github.com/lowcode-workspace/awesome-lowcode-engine) page links to a repository which records all of the tools\materials\solutions that use or built for the lowcode-engine, PR is welcomed. @@ -146,14 +155,14 @@ $ npm start > > 📢 Windows environment must use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install), other terminals are not guaranteed to work normally -After lowcode-engine is started, several umd files are provided, which can be debugged in combination with the [lowcode-demo](https://github.com/alibaba/lowcode-demo) project. Refer to the file proxy rules [here](https://www.yuque.com/lce/doc/glz0fx). +After lowcode-engine is started, several umd files are provided, which can be debugged in combination with the [lowcode-demo](https://github.com/alibaba/lowcode-demo) project. Refer to the file proxy rules [here](https://lowcode-engine.cn/site/docs/participate/prepare). ## 🤝 Participation Please read first: -1. [How to configure the engine debugging environment? ](https://www.yuque.com/lce/doc/glz0fx) -2. [About the R&D collaboration process of the engine](https://www.yuque.com/lce/doc/contributing) -3. [Engineering Configuration of Engine](https://www.yuque.com/lce/doc/gxwqg6) +1. [How to configure the engine debugging environment? ](https://lowcode-engine.cn/site/docs/participate/prepare) +2. [About the R&D collaboration process of the engine](https://lowcode-engine.cn/site/docs/participate/flow) +3. [Engineering Configuration of Engine](https://lowcode-engine.cn/site/docs/participate/config) > Strongly recommend reading ["The Wisdom of Asking Questions"](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way), ["How to Ask Questions to the Open Source Community"](https: //github.com/seajs/seajs/issues/545) and [How to Report Bugs Effectively](http://www.chiark.greenend.org.uk/%7Esgtatham/bugs-cn.html), [ "How to Submit Unanswerable Questions to Open Source Projects"](https://zhuanlan.zhihu.com/p/25795393), better questions are easier to get help. (This paragraph refers to [antd](https://github.com/ant-design/ant-design)) @@ -166,4 +175,4 @@ Special thanks to everyone who contributed to this project. <p> <a href="https://github.com/alibaba/lowcode-engine/graphs/contributors"><img src="https://contrib.rocks/image?repo=alibaba/lowcode-engine" /></a> -</p> \ No newline at end of file +</p> diff --git a/packages/engine/babel.config.js b/packages/engine/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/packages/engine/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/engine/build.json b/packages/engine/build.json index e8c3e583de..405f2c005c 100644 --- a/packages/engine/build.json +++ b/packages/engine/build.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", [ "build-plugin-fusion", { diff --git a/packages/engine/build.test.json b/packages/engine/build.test.json index a43d8eff9f..9596d43e79 100644 --- a/packages/engine/build.test.json +++ b/packages/engine/build.test.json @@ -1,7 +1,7 @@ { "plugins": [ [ - "build-plugin-component", + "@alilc/build-plugin-lce", { "filename": "editor-preset-vision", "library": "LowcodeEditor", diff --git a/packages/engine/package.json b/packages/engine/package.json index f348a9bc3c..23b3521213 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-engine", - "version": "1.0.15", + "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", @@ -12,21 +12,23 @@ "scripts": { "start": "build-scripts start", "version:update": "node ./scripts/version.js", - "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --skip-demo", + "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build", "build:umd": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --config build.umd.json", "test": "build-scripts test --config build.test.json --jest-passWithNoTests" }, "license": "MIT", "dependencies": { "@alifd/next": "^1.19.12", - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-editor-skeleton": "1.0.15", + "@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.0.15", - "@alilc/lowcode-plugin-outline-pane": "1.0.15", - "@alilc/lowcode-shell": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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" }, @@ -34,7 +36,6 @@ "@alib/build-scripts": "^0.1.18", "@alifd/theme-lowcode-dark": "^0.2.0", "@alifd/theme-lowcode-light": "^0.2.0", - "@alilc/lowcode-test-mate": "^1.0.1", "@types/domready": "^1.0.0", "@types/react": "^16.8.3", "@types/react-dom": "^16.8.2", @@ -53,5 +54,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/engine" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/engine/src/engine-core.ts b/packages/engine/src/engine-core.ts index aad924324b..4dffa628bd 100644 --- a/packages/engine/src/engine-core.ts +++ b/packages/engine/src/engine-core.ts @@ -1,74 +1,188 @@ +/* eslint-disable max-len */ +/* eslint-disable no-param-reassign */ import { createElement } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { globalContext, Editor, engineConfig, EngineOptions } from '@alilc/lowcode-editor-core'; +import { + globalContext, + Editor, + commonEvent, + engineConfig, + Setters as InnerSetters, + Hotkey as InnerHotkey, + IEditor, + Command as InnerCommand, +} from '@alilc/lowcode-editor-core'; +import { + IPublicTypeEngineOptions, + IPublicModelDocumentModel, + IPublicTypePluginMeta, + IPublicTypeDisposable, + IPublicApiPlugins, + IPublicApiWorkspace, + IPublicEnumPluginRegisterLevel, + IPublicModelPluginContext, +} from '@alilc/lowcode-types'; import { Designer, LowCodePluginManager, - ILowCodePluginContext, + ILowCodePluginContextPrivate, + ILowCodePluginContextApiAssembler, PluginPreference, - TransformStage, + IDesigner, } from '@alilc/lowcode-designer'; import { Skeleton as InnerSkeleton, - SettingsPrimaryPane, registerDefaults, } from '@alilc/lowcode-editor-skeleton'; +import { + Workspace as InnerWorkspace, + Workbench as WorkSpaceWorkbench, + IWorkspace, +} from '@alilc/lowcode-workspace'; -import Outline, { OutlineBackupPane, getTreeMaster } from '@alilc/lowcode-plugin-outline-pane'; -import DesignerPlugin from '@alilc/lowcode-plugin-designer'; -import { Hotkey, Project, Skeleton, Setters, Material, Event, DocumentModel } from '@alilc/lowcode-shell'; -import { getLogger, isPlainObject } from '@alilc/lowcode-utils'; +import { + Hotkey, + Project, + Skeleton, + Setters, + Material, + Event, + Plugins, + Common, + Logger, + Canvas, + Workspace, + Config, + CommonUI, + Command, +} from '@alilc/lowcode-shell'; +import { isPlainObject } from '@alilc/lowcode-utils'; import './modules/live-editing'; -import utils from './modules/utils'; -import * as editorCabin from './modules/editor-cabin'; -import getSkeletonCabin from './modules/skeleton-cabin'; -import getDesignerCabin from './modules/designer-cabin'; -import classes from './modules/classes'; +import * as classes from './modules/classes'; import symbols from './modules/symbols'; -export * from './modules/editor-types'; +import { componentMetaParser } from './inner-plugins/component-meta-parser'; +import { setterRegistry } from './inner-plugins/setter-registry'; +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'; export * from './modules/designer-types'; export * from './modules/lowcode-types'; -registerDefaults(); +async function registryInnerPlugin(designer: IDesigner, editor: IEditor, plugins: IPublicApiPlugins): Promise<IPublicTypeDisposable> { + // 注册一批内置插件 + const componentMetaParserPlugin = componentMetaParser(designer); + const defaultPanelRegistryPlugin = defaultPanelRegistry(editor); + await plugins.register(OutlinePlugin, {}, { autoInit: true }); + await plugins.register(componentMetaParserPlugin); + await plugins.register(setterRegistry, {}); + await plugins.register(defaultPanelRegistryPlugin); + await plugins.register(builtinHotkey); + await plugins.register(registerDefaults, {}, { autoInit: true }); + await plugins.register(defaultContextMenu); + await plugins.register(CommandPlugin, {}); + return () => { + plugins.delete(OutlinePlugin.pluginName); + plugins.delete(componentMetaParserPlugin.pluginName); + plugins.delete(setterRegistry.pluginName); + plugins.delete(defaultPanelRegistryPlugin.pluginName); + plugins.delete(builtinHotkey.pluginName); + plugins.delete(registerDefaults.pluginName); + plugins.delete(defaultContextMenu.pluginName); + plugins.delete(CommandPlugin.pluginName); + }; +} + +const innerWorkspace: IWorkspace = new InnerWorkspace(registryInnerPlugin, shellModelFactory); +const workspace: IPublicApiWorkspace = new Workspace(innerWorkspace); const editor = new Editor(); globalContext.register(editor, Editor); globalContext.register(editor, 'editor'); +globalContext.register(innerWorkspace, 'workspace'); + +const engineContext: Partial<ILowCodePluginContextPrivate> = {}; const innerSkeleton = new InnerSkeleton(editor); editor.set('skeleton' as any, innerSkeleton); -const designer = new Designer({ editor }); +const designer = new Designer({ editor, shellModelFactory }); editor.set('designer' as any, designer); -const plugins = new LowCodePluginManager(editor).toProxy(); -editor.set('plugins' as any, plugins); - const { project: innerProject } = designer; -const skeletonCabin = getSkeletonCabin(innerSkeleton); -const { Workbench } = skeletonCabin; -const hotkey = new Hotkey(); +const innerHotkey = new InnerHotkey(); +const hotkey = new Hotkey(innerHotkey); const project = new Project(innerProject); -const skeleton = new Skeleton(innerSkeleton); -const setters = new Setters(); +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 config = engineConfig; -const event = new Event(editor, { prefix: 'common' }); -const logger = getLogger({ level: 'warn', bizName: 'common' }); -const designerCabin = getDesignerCabin(editor); -const objects = { - TransformStage, -}; -const common = { - utils, - objects, - editorCabin, - designerCabin, - skeletonCabin, +const commonUI = new CommonUI(editor); +editor.set('project', project); +editor.set('setters' as any, setters); +editor.set('material', material); +editor.set('innerHotkey', innerHotkey); +const config = new Config(engineConfig); +const event = new Event(commonEvent, { prefix: 'common' }); +const logger = new Logger({ level: 'warn', bizName: 'common' }); +const common = new Common(editor, innerSkeleton); +const canvas = new Canvas(editor); +let plugins: Plugins; + +const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + assembleApis: (context: ILowCodePluginContextPrivate, pluginName: string, meta: IPublicTypePluginMeta) => { + context.hotkey = hotkey; + context.project = project; + context.skeleton = new Skeleton(innerSkeleton, pluginName, false); + 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; + context.canvas = canvas; + context.plugins = plugins; + 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); + }, }; +const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler); +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, @@ -80,8 +194,10 @@ export { logger, hotkey, common, - // 兼容原 editor 的事件功能 - event as editor, + workspace, + canvas, + commonUI, + command, }; // declare this is open-source version export const isOpenSource = true; @@ -91,107 +207,17 @@ export const __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = { }; engineConfig.set('isOpenSource', isOpenSource); -// 注册一批内置插件 -(async function registerPlugins() { - // 处理 editor.set('assets'),将组件元数据创建好 - const componentMetaParser = (ctx: ILowCodePluginContext) => { - return { - init() { - editor.onGot('assets', (assets: any) => { - const { components = [] } = assets; - designer.buildComponentMetasMap(components); - }); - }, - }; - }; - componentMetaParser.pluginName = '___component_meta_parser___'; - await plugins.register(componentMetaParser); - - // 注册默认的 setters - const setterRegistry = (ctx: ILowCodePluginContext) => { - return { - init() { - if (engineConfig.get('disableDefaultSetters')) return; - const builtinSetters = require('@alilc/lowcode-engine-ext')?.setters; - if (builtinSetters) { - ctx.setters.registerSetter(builtinSetters); - } - }, - }; - }; - setterRegistry.pluginName = '___setter_registry___'; - await plugins.register(setterRegistry); - - // 注册默认的面板 - const defaultPanelRegistry = (ctx: ILowCodePluginContext) => { - return { - init() { - skeleton.add({ - area: 'mainArea', - name: 'designer', - type: 'Widget', - content: DesignerPlugin, - }); - if (!engineConfig.get('disableDefaultSettingPanel')) { - skeleton.add({ - area: 'rightArea', - name: 'settingsPane', - type: 'Panel', - content: SettingsPrimaryPane, - props: { - ignoreRoot: true, - }, - }); - } - - // by default in float area; - let isInFloatArea = true; - const hasPreferenceForOutline = editor - ?.getPreference() - ?.contains('outline-pane-pinned-status-isFloat', 'skeleton'); - if (hasPreferenceForOutline) { - isInFloatArea = editor - ?.getPreference() - ?.get('outline-pane-pinned-status-isFloat', 'skeleton'); - } - - skeleton.add({ - area: 'leftArea', - name: 'outlinePane', - type: 'PanelDock', - content: Outline, - panelProps: { - area: isInFloatArea ? 'leftFloatArea' : 'leftFixedArea', - keepVisibleWhileDragging: true, - ...engineConfig.get('defaultOutlinePaneProps'), - }, - }); - skeleton.add({ - area: 'rightArea', - name: 'backupOutline', - type: 'Panel', - props: { - condition: () => { - return designer.dragon.dragging && !getTreeMaster(designer).hasVisibleTreeBoard(); - }, - }, - content: OutlineBackupPane, - }); - }, - }; - }; - defaultPanelRegistry.pluginName = '___default_panel___'; - await plugins.register(defaultPanelRegistry); -})(); - // container which will host LowCodeEngine DOM let engineContainer: HTMLElement; // @ts-ignore webpack Define variable export const version = VERSION_PLACEHOLDER; engineConfig.set('ENGINE_VERSION', version); + +const pluginPromise = registryInnerPlugin(designer, editor, plugins); + export async function init( container?: HTMLElement, - options?: EngineOptions, + options?: IPublicTypeEngineOptions, pluginPreference?: PluginPreference, ) { await destroy(); @@ -212,7 +238,29 @@ export async function init( } engineConfig.setEngineOptions(engineOptions as any); + const { Workbench } = common.skeletonCabin; + if (options && options.enableWorkspaceMode) { + const disposeFun = await pluginPromise; + disposeFun && disposeFun(); + render( + createElement(WorkSpaceWorkbench, { + workspace: innerWorkspace, + // skeleton: workspace.skeleton, + className: 'engine-main', + topAreaItemClassName: 'engine-actionitem', + }), + engineContainer, + ); + innerWorkspace.enableAutoOpenFirstWindow = engineConfig.get('enableAutoOpenFirstWindow', true); + innerWorkspace.setActive(true); + innerWorkspace.initWindow(); + innerHotkey.activate(false); + await innerWorkspace.plugins.init(pluginPreference); + return; + } + await plugins.init(pluginPreference as any); + render( createElement(Workbench, { skeleton: innerSkeleton, @@ -227,7 +275,7 @@ export async function destroy() { // remove all documents const { documents } = project; if (Array.isArray(documents) && documents.length > 0) { - documents.forEach(((doc: DocumentModel) => project.removeDocument(doc))); + documents.forEach(((doc: IPublicModelDocumentModel) => project.removeDocument(doc))); } // TODO: delete plugins except for core plugins diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts index aa1ba057ee..fe57036eb1 100644 --- a/packages/engine/src/index.ts +++ b/packages/engine/src/index.ts @@ -1,6 +1,6 @@ import { version } from './engine-core'; -export * from './engine-core'; +export * from './engine-core'; console.log( `%c AliLowCodeEngine %c v${version} `, 'padding: 2px 1px; border-radius: 3px 0 0 3px; color: #fff; background: #606060; font-weight: bold;', diff --git a/packages/engine/src/inner-plugins/builtin-hotkey.ts b/packages/engine/src/inner-plugins/builtin-hotkey.ts new file mode 100644 index 0000000000..1a1f3a9c44 --- /dev/null +++ b/packages/engine/src/inner-plugins/builtin-hotkey.ts @@ -0,0 +1,550 @@ +/* eslint-disable max-len */ +import { isFormEvent, isNodeSchema, isNode } from '@alilc/lowcode-utils'; +import { + IPublicModelPluginContext, + IPublicEnumTransformStage, + IPublicModelNode, + IPublicTypeNodeSchema, + IPublicTypeNodeData, + IPublicEnumDragObjectType, + IPublicTypeDragNodeObject, +} from '@alilc/lowcode-types'; + +function insertChild( + container: IPublicModelNode, + originalChild: IPublicModelNode | IPublicTypeNodeData, + at?: number | null, +): IPublicModelNode | null { + let child = originalChild; + if (isNode(child) && (child as IPublicModelNode).isSlotNode) { + child = (child as IPublicModelNode).exportSchema(IPublicEnumTransformStage.Clone); + } + let node = null; + if (isNode(child)) { + node = (child as IPublicModelNode); + container.children?.insert(node, at); + } else { + node = container.document?.createNode(child) || null; + if (node) { + container.children?.insert(node, at); + } + } + + return (node as IPublicModelNode) || null; +} + +function insertChildren( + container: IPublicModelNode, + nodes: IPublicModelNode[] | IPublicTypeNodeData[], + at?: number | null, +): IPublicModelNode[] { + let index = at; + let node: any; + const results: IPublicModelNode[] = []; + // eslint-disable-next-line no-cond-assign + while ((node = nodes.pop())) { + node = insertChild(container, node, index); + results.push(node); + index = node.index; + } + return results; +} + +/** + * 获得合适的插入位置 + */ +function getSuitableInsertion( + pluginContext: IPublicModelPluginContext, + insertNode?: IPublicModelNode | IPublicTypeNodeSchema | IPublicTypeNodeSchema[], +): { target: IPublicModelNode; index?: number } | null { + const { project, material } = pluginContext; + const activeDoc = project.currentDocument; + if (!activeDoc) { + return null; + } + if ( + Array.isArray(insertNode) && + isNodeSchema(insertNode[0]) && + material.getComponentMeta(insertNode[0].componentName)?.isModal + ) { + if (!activeDoc.root) { + return null; + } + + return { + target: activeDoc.root, + }; + } + + const focusNode = activeDoc.focusNode!; + const nodes = activeDoc.selection.getNodes(); + const refNode = nodes.find((item) => focusNode.contains(item)); + let target; + let index: number | undefined; + if (!refNode || refNode === focusNode) { + target = focusNode; + } else if (refNode.componentMeta?.isContainer) { + target = refNode; + } else { + // FIXME!!, parent maybe null + target = refNode.parent!; + index = refNode.index + 1; + } + + if (target && insertNode && !target.componentMeta?.checkNestingDown(target, insertNode)) { + return null; + } + + return { target, index }; +} + +/* istanbul ignore next */ +function getNextForSelect(next: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any { + if (next) { + if (!head) { + return next; + } + + let ret; + if (next.isContainerNode) { + const { children } = next; + if (children && !children.isEmptyNode) { + ret = getNextForSelect(children.get(0)); + if (ret) { + return ret; + } + } + } + + ret = getNextForSelect(next.nextSibling); + if (ret) { + return ret; + } + } + + if (parent) { + return getNextForSelect(parent.nextSibling, false, parent?.parent); + } + + return null; +} + +/* istanbul ignore next */ +function getPrevForSelect(prev: IPublicModelNode | null, head?: any, parent?: IPublicModelNode | null): any { + if (prev) { + let ret; + if (!head && prev.isContainerNode) { + const { children } = prev; + const lastChild = children && !children.isEmptyNode ? children.get(children.size - 1) : null; + + ret = getPrevForSelect(lastChild); + if (ret) { + return ret; + } + } + + if (!head) { + return prev; + } + + ret = getPrevForSelect(prev.prevSibling); + if (ret) { + return ret; + } + } + + if (parent) { + return parent; + } + + return null; +} + +function getSuitablePlaceForNode(targetNode: IPublicModelNode, node: IPublicModelNode, ref: any): any { + const { document } = targetNode; + if (!document) { + return null; + } + + const dragNodeObject: IPublicTypeDragNodeObject = { + type: IPublicEnumDragObjectType.Node, + nodes: [node], + }; + + const focusNode = document?.focusNode; + // 如果节点是模态框,插入到根节点下 + if (node?.componentMeta?.isModal) { + return { container: focusNode, ref }; + } + + if (!ref && focusNode && targetNode.contains(focusNode)) { + if (document.checkNesting(focusNode, dragNodeObject)) { + return { container: focusNode }; + } + + return null; + } + + if (targetNode.isRootNode && targetNode.children) { + const dropElement = targetNode.children.filter((c) => { + if (!c.isContainerNode) { + return false; + } + if (document.checkNesting(c, dragNodeObject)) { + return true; + } + return false; + })[0]; + + if (dropElement) { + return { container: dropElement, ref }; + } + + if (document.checkNesting(targetNode, dragNodeObject)) { + return { container: targetNode, ref }; + } + + return null; + } + + if (targetNode.isContainerNode) { + if (document.checkNesting(targetNode, dragNodeObject)) { + return { container: targetNode, ref }; + } + } + + if (targetNode.parent) { + return getSuitablePlaceForNode(targetNode.parent, node, { index: targetNode.index }); + } + + return null; +} + +// 注册默认的 setters +export const builtinHotkey = (ctx: IPublicModelPluginContext) => { + return { + init() { + const { hotkey, project, logger, canvas } = ctx; + const { clipboard } = canvas; + // hotkey binding + hotkey.bind(['backspace', 'del'], (e: KeyboardEvent, action) => { + logger.info(`action ${action} is triggered`); + + if (canvas.isInLiveEditing) { + return; + } + // TODO: use focus-tracker + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + + const sel = doc.selection; + const topItems = sel.getTopNodes(); + // TODO: check can remove + topItems.forEach((node) => { + if (node?.canPerformAction('remove')) { + node && doc.removeNode(node); + } + }); + sel.clear(); + }); + + hotkey.bind('escape', (e: KeyboardEvent, action) => { + logger.info(`action ${action} is triggered`); + + if (canvas.isInLiveEditing) { + return; + } + const sel = project.currentDocument?.selection; + if (isFormEvent(e) || !sel) { + return; + } + e.preventDefault(); + + sel.clear(); + // currentFocus.esc(); + }); + + // command + c copy command + x cut + hotkey.bind(['command+c', 'ctrl+c', 'command+x', 'ctrl+x'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + const anchorValue = document.getSelection()?.anchorNode?.nodeValue; + if (anchorValue && typeof anchorValue === 'string') { + return; + } + e.preventDefault(); + + let selected = doc.selection.getTopNodes(true); + selected = selected.filter((node) => { + return node?.canPerformAction('copy'); + }); + if (!selected || selected.length < 1) { + return; + } + + const componentsMap = {}; + const componentsTree = selected.map((item) => item?.exportSchema(IPublicEnumTransformStage.Clone)); + + // FIXME: clear node.id + + const data = { type: 'nodeSchema', componentsMap, componentsTree }; + + clipboard.setData(data); + + const cutMode = action && action.indexOf('x') > 0; + if (cutMode) { + selected.forEach((node) => { + const parentNode = node?.parent; + parentNode?.select(); + node?.remove(); + }); + } + }); + + // command + v paste + hotkey.bind(['command+v', 'ctrl+v'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + // TODO + const doc = project?.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + /* istanbul ignore next */ + clipboard.waitPasteData(e, ({ componentsTree }) => { + if (componentsTree) { + const { target, index } = getSuitableInsertion(ctx, componentsTree) || {}; + if (!target) { + return; + } + let canAddComponentsTree = componentsTree.filter((node: IPublicModelNode) => { + const dragNodeObject: IPublicTypeDragNodeObject = { + type: IPublicEnumDragObjectType.Node, + nodes: [node], + }; + return doc.checkNesting(target, dragNodeObject); + }); + if (canAddComponentsTree.length === 0) { + return; + } + const nodes = insertChildren(target, canAddComponentsTree, index); + if (nodes) { + doc.selection.selectAll(nodes.map((o) => o.id)); + setTimeout(() => canvas.activeTracker?.track(nodes[0]), 10); + } + } + }); + }); + + // command + z undo + hotkey.bind(['command+z', 'ctrl+z'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const history = project.currentDocument?.history; + if (isFormEvent(e) || !history) { + return; + } + + e.preventDefault(); + const selection = project.currentDocument?.selection; + const curSelected = selection?.selected && Array.from(selection?.selected); + history.back(); + selection?.selectAll(curSelected); + }); + + // command + shift + z redo + hotkey.bind(['command+y', 'ctrl+y', 'command+shift+z'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const history = project.currentDocument?.history; + if (isFormEvent(e) || !history) { + return; + } + e.preventDefault(); + const selection = project.currentDocument?.selection; + const curSelected = selection?.selected && Array.from(selection?.selected); + history.forward(); + selection?.selectAll(curSelected); + }); + + // sibling selection + hotkey.bind(['left', 'right'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + const firstNode = selected[0]; + const silbing = action === 'left' ? firstNode?.prevSibling : firstNode?.nextSibling; + silbing?.select(); + }); + + hotkey.bind(['up', 'down'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + const firstNode = selected[0]; + + if (action === 'down') { + const next = getNextForSelect(firstNode, true, firstNode?.parent); + next?.select(); + } else if (action === 'up') { + const prev = getPrevForSelect(firstNode, true, firstNode?.parent); + prev?.select(); + } + }); + + hotkey.bind(['option+left', 'option+right'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 + // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 + + const firstNode = selected[0]; + const parent = firstNode?.parent; + if (!parent) return; + + const isPrev = action && /(left)$/.test(action); + + const silbing = isPrev ? firstNode.prevSibling : firstNode.nextSibling; + if (silbing) { + if (isPrev) { + parent.insertBefore(firstNode, silbing, true); + } else { + parent.insertAfter(firstNode, silbing, true); + } + firstNode?.select(); + } + }); + + hotkey.bind(['option+up'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.currentDocument; + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + // TODO: 此处需要增加判断当前节点是否可被操作移动,原ve里是用 node.canOperating()来判断 + // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 + + const firstNode = selected[0]; + const parent = firstNode?.parent; + if (!parent) { + return; + } + + const silbing = firstNode.prevSibling; + if (silbing) { + if (silbing.isContainerNode) { + const place = getSuitablePlaceForNode(silbing, firstNode, null); + silbing.insertAfter(firstNode, place.ref, true); + } else { + parent.insertBefore(firstNode, silbing, true); + } + firstNode?.select(); + } else { + const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards + if (place) { + const container = place.container.internalToShellNode(); + container.insertBefore(firstNode, place.ref); + firstNode?.select(); + } + } + }); + + hotkey.bind(['option+down'], (e, action) => { + logger.info(`action ${action} is triggered`); + if (canvas.isInLiveEditing) { + return; + } + const doc = project.getCurrentDocument(); + if (isFormEvent(e) || !doc) { + return; + } + e.preventDefault(); + const selected = doc.selection.getTopNodes(true); + if (!selected || selected.length < 1) { + return; + } + // TODO: 此处需要增加判断当前节点是否可被操作移动,原 ve 里是用 node.canOperating() 来判断 + // TODO: 移动逻辑也需要重新梳理,对于移动目标位置的选择,是否可以移入,需要增加判断 + + const firstNode = selected[0]; + const parent = firstNode?.parent; + if (!parent) { + return; + } + + const silbing = firstNode.nextSibling; + if (silbing) { + if (silbing.isContainerNode) { + silbing.insertBefore(firstNode, undefined); + } else { + parent.insertAfter(firstNode, silbing, true); + } + firstNode?.select(); + } else { + const place = getSuitablePlaceForNode(parent, firstNode, null); // upwards + if (place) { + const container = place.container.internalToShellNode(); + container.insertAfter(firstNode, place.ref, true); + firstNode?.select(); + } + } + }); + }, + }; +}; + +builtinHotkey.pluginName = '___builtin_hotkey___'; diff --git a/packages/engine/src/inner-plugins/component-meta-parser.ts b/packages/engine/src/inner-plugins/component-meta-parser.ts new file mode 100644 index 0000000000..d0fbb4300f --- /dev/null +++ b/packages/engine/src/inner-plugins/component-meta-parser.ts @@ -0,0 +1,20 @@ +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +export const componentMetaParser = (designer: any) => { + const fun = (ctx: IPublicModelPluginContext) => { + return { + init() { + const { material } = ctx; + material.onChangeAssets(() => { + const assets = material.getAssets(); + const { components = [] } = assets; + designer.buildComponentMetasMap(components); + }); + }, + }; + }; + + fun.pluginName = '___component_meta_parser___'; + + return fun; +}; diff --git a/packages/engine/src/inner-plugins/default-context-menu.ts b/packages/engine/src/inner-plugins/default-context-menu.ts new file mode 100644 index 0000000000..81978d9209 --- /dev/null +++ b/packages/engine/src/inner-plugins/default-context-menu.ts @@ -0,0 +1,223 @@ +import { + IPublicEnumContextMenuType, + IPublicEnumDragObjectType, + IPublicEnumTransformStage, + IPublicModelNode, + IPublicModelPluginContext, + IPublicTypeDragNodeDataObject, + IPublicTypeNodeSchema, +} from '@alilc/lowcode-types'; +import { isProjectSchema } from '@alilc/lowcode-utils'; +import { Message } from '@alifd/next'; +import { intl } from '../locale'; + +function getNodesSchema(nodes: IPublicModelNode[]) { + const componentsTree = nodes.map((node) => node?.exportSchema(IPublicEnumTransformStage.Clone)); + const data = { type: 'nodeSchema', componentsMap: {}, componentsTree }; + return data; +} + +async function getClipboardText(): Promise<IPublicTypeNodeSchema[]> { + return new Promise((resolve, reject) => { + // 使用 Clipboard API 读取剪贴板内容 + navigator.clipboard.readText().then( + (text) => { + try { + const data = JSON.parse(text); + if (isProjectSchema(data)) { + resolve(data.componentsTree); + } else { + Message.error(intl('NotValidNodeData')); + reject( + new Error(intl('NotValidNodeData')), + ); + } + } catch (error) { + Message.error(intl('NotValidNodeData')); + reject(error); + } + }, + (err) => { + reject(err); + }, + ); + }); +} + +export const defaultContextMenu = (ctx: IPublicModelPluginContext) => { + const { material, canvas, common } = ctx; + const { clipboard } = canvas; + const { intl: utilsIntl } = common.utils; + + return { + init() { + material.addContextMenuOption({ + name: 'selectComponent', + title: intl('SelectComponents'), + condition: (nodes = []) => { + return nodes.length === 1; + }, + items: [ + { + name: 'nodeTree', + type: IPublicEnumContextMenuType.NODE_TREE, + }, + ], + }); + + material.addContextMenuOption({ + name: 'copyAndPaste', + title: intl('CopyAndPaste'), + disabled: (nodes = []) => { + return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; + }, + condition: (nodes) => { + return nodes?.length === 1; + }, + action(nodes) { + const node = nodes?.[0]; + if (!node) { + return; + } + const { document: doc, parent, index } = node; + const data = getNodesSchema(nodes); + clipboard.setData(data); + + if (parent) { + const newNode = doc?.insertNode(parent, node, (index ?? 0) + 1, true); + newNode?.select(); + } + }, + }); + + material.addContextMenuOption({ + name: 'copy', + title: intl('Copy'), + disabled: (nodes = []) => { + return nodes?.filter((node) => !node?.canPerformAction('copy')).length > 0; + }, + condition(nodes = []) { + return nodes?.length > 0; + }, + action(nodes) { + if (!nodes || nodes.length < 1) { + return; + } + + const data = getNodesSchema(nodes); + clipboard.setData(data); + }, + }); + + material.addContextMenuOption({ + name: 'pasteToBottom', + title: intl('PasteToTheBottom'), + condition: (nodes) => { + return nodes?.length === 1; + }, + async action(nodes) { + if (!nodes || nodes.length < 1) { + return; + } + + const node = nodes[0]; + const { document: doc, parent, index } = node; + + try { + const nodeSchema = await getClipboardText(); + if (nodeSchema.length === 0) { + return; + } + if (parent) { + let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { + const dragNodeObject: IPublicTypeDragNodeDataObject = { + type: IPublicEnumDragObjectType.NodeData, + data: nodeSchema, + }; + return doc?.checkNesting(parent, dragNodeObject); + }); + if (canAddNodes.length === 0) { + Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(parent.title || parent.componentName as any)}内`); + return; + } + const nodes: IPublicModelNode[] = []; + canAddNodes.forEach((schema, schemaIndex) => { + const node = doc?.insertNode(parent, schema, (index ?? 0) + 1 + schemaIndex, true); + node && nodes.push(node); + }); + doc?.selection.selectAll(nodes.map((node) => node?.id)); + } + } catch (error) { + console.error(error); + } + }, + }); + + material.addContextMenuOption({ + name: 'pasteToInner', + title: intl('PasteToTheInside'), + condition: (nodes) => { + return nodes?.length === 1; + }, + disabled: (nodes = []) => { + // 获取粘贴数据 + const node = nodes?.[0]; + return !node.isContainerNode; + }, + async action(nodes) { + const node = nodes?.[0]; + if (!node) { + return; + } + const { document: doc } = node; + + try { + const nodeSchema = await getClipboardText(); + const index = node.children?.size || 0; + if (nodeSchema.length === 0) { + return; + } + let canAddNodes = nodeSchema.filter((nodeSchema: IPublicTypeNodeSchema) => { + const dragNodeObject: IPublicTypeDragNodeDataObject = { + type: IPublicEnumDragObjectType.NodeData, + data: nodeSchema, + }; + return doc?.checkNesting(node, dragNodeObject); + }); + if (canAddNodes.length === 0) { + Message.error(`${nodeSchema.map(d => utilsIntl(d.title || d.componentName)).join(',')}等组件无法放置到${utilsIntl(node.title || node.componentName as any)}内`); + return; + } + + const nodes: IPublicModelNode[] = []; + nodeSchema.forEach((schema, schemaIndex) => { + const newNode = doc?.insertNode(node, schema, (index ?? 0) + 1 + schemaIndex, true); + newNode && nodes.push(newNode); + }); + doc?.selection.selectAll(nodes.map((node) => node?.id)); + } catch (error) { + console.error(error); + } + }, + }); + + material.addContextMenuOption({ + name: 'delete', + title: intl('Delete'), + disabled(nodes = []) { + return nodes?.filter((node) => !node?.canPerformAction('remove')).length > 0; + }, + condition(nodes = []) { + return nodes.length > 0; + }, + action(nodes) { + nodes?.forEach((node) => { + node.remove(); + }); + }, + }); + }, + }; +}; + +defaultContextMenu.pluginName = '___default_context_menu___'; diff --git a/packages/engine/src/inner-plugins/default-panel-registry.tsx b/packages/engine/src/inner-plugins/default-panel-registry.tsx new file mode 100644 index 0000000000..b5f538d44c --- /dev/null +++ b/packages/engine/src/inner-plugins/default-panel-registry.tsx @@ -0,0 +1,45 @@ +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; +import { SettingsPrimaryPane } from '@alilc/lowcode-editor-skeleton'; +import DesignerPlugin from '@alilc/lowcode-plugin-designer'; + +// 注册默认的面板 +export const defaultPanelRegistry = (editor: any) => { + const fun = (ctx: IPublicModelPluginContext) => { + return { + init() { + const { skeleton, config } = ctx; + skeleton.add({ + area: 'mainArea', + name: 'designer', + type: 'Widget', + content: <DesignerPlugin + engineConfig={config} + engineEditor={editor} + />, + }); + if (!config.get('disableDefaultSettingPanel')) { + skeleton.add({ + area: 'rightArea', + name: 'settingsPane', + type: 'Panel', + content: <SettingsPrimaryPane + engineEditor={editor} + />, + props: { + ignoreRoot: true, + }, + panelProps: { + ...(config.get('defaultSettingPanelProps') || {}), + }, + }); + } + }, + }; + }; + + fun.pluginName = '___default_panel___'; + + return fun; +}; + +export default defaultPanelRegistry; diff --git a/packages/engine/src/inner-plugins/setter-registry.ts b/packages/engine/src/inner-plugins/setter-registry.ts new file mode 100644 index 0000000000..9d7fe4fccf --- /dev/null +++ b/packages/engine/src/inner-plugins/setter-registry.ts @@ -0,0 +1,17 @@ +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +// 注册默认的 setters +export const setterRegistry = (ctx: IPublicModelPluginContext) => { + return { + init() { + const { config } = ctx; + if (config.get('disableDefaultSetters')) return; + const builtinSetters = require('@alilc/lowcode-engine-ext')?.setters; + if (builtinSetters) { + ctx.setters.registerSetter(builtinSetters); + } + }, + }; +}; + +setterRegistry.pluginName = '___setter_registry___'; diff --git a/packages/engine/src/locale/en-US.json b/packages/engine/src/locale/en-US.json new file mode 100644 index 0000000000..e931607073 --- /dev/null +++ b/packages/engine/src/locale/en-US.json @@ -0,0 +1,9 @@ +{ + "NotValidNodeData": "Not valid node data", + "SelectComponents": "Select components", + "CopyAndPaste": "Copy and Paste", + "Copy": "Copy", + "PasteToTheBottom": "Paste to the bottom", + "PasteToTheInside": "Paste to the inside", + "Delete": "Delete" +} diff --git a/packages/engine/src/locale/index.ts b/packages/engine/src/locale/index.ts new file mode 100644 index 0000000000..ca89840b05 --- /dev/null +++ b/packages/engine/src/locale/index.ts @@ -0,0 +1,14 @@ +import { createIntl } from '@alilc/lowcode-editor-core'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const { intl, getLocale } = createIntl?.({ + 'en-US': enUS, + 'zh-CN': zhCN, +}) || { + intl: (id) => { + return zhCN[id]; + }, +}; + +export { intl, enUS, zhCN, getLocale }; diff --git a/packages/engine/src/locale/zh-CN.json b/packages/engine/src/locale/zh-CN.json new file mode 100644 index 0000000000..9b68b71490 --- /dev/null +++ b/packages/engine/src/locale/zh-CN.json @@ -0,0 +1,9 @@ +{ + "NotValidNodeData": "不是有效的节点数据", + "SelectComponents": "选择组件", + "CopyAndPaste": "复制", + "Copy": "拷贝", + "PasteToTheBottom": "粘贴至下方", + "PasteToTheInside": "粘贴至内部", + "Delete": "删除" +} diff --git a/packages/engine/src/modules/classes.ts b/packages/engine/src/modules/classes.ts index 11d1e7089f..3b7627deb0 100644 --- a/packages/engine/src/modules/classes.ts +++ b/packages/engine/src/modules/classes.ts @@ -1,4 +1,4 @@ -import { +export { Project, Skeleton, DocumentModel, @@ -8,18 +8,9 @@ import { SettingPropEntry, SettingTopEntry, Selection, + Prop, + SimulatorHost, + SkeletonItem, } from '@alilc/lowcode-shell'; -import { Node as InnerNode } from '@alilc/lowcode-designer'; +export { Node as InnerNode } from '@alilc/lowcode-designer'; -export default { - Project, - Skeleton, - DocumentModel, - Node, - NodeChildren, - History, - SettingPropEntry, - SettingTopEntry, - InnerNode, - Selection, -}; diff --git a/packages/engine/src/modules/designer-cabin.ts b/packages/engine/src/modules/designer-cabin.ts deleted file mode 100644 index 812f53a764..0000000000 --- a/packages/engine/src/modules/designer-cabin.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { - SettingField, - isSettingField, - Designer, - TransformStage, - LiveEditing, - isDragNodeDataObject, - DragObjectType, - isNode, -} from '@alilc/lowcode-designer'; -import { Editor } from '@alilc/lowcode-editor-core'; -import { Dragon } from '@alilc/lowcode-shell'; - -export default function getDesignerCabin(editor: Editor) { - const designer = editor.get('designer') as Designer; - - return { - SettingField, - isSettingField, - dragon: Dragon.create(designer.dragon), - TransformStage, - LiveEditing, - DragObjectType, - isDragNodeDataObject, - isNode, - }; -} \ No newline at end of file diff --git a/packages/engine/src/modules/designer-types.ts b/packages/engine/src/modules/designer-types.ts index abcb418d7e..a0ee79952c 100644 --- a/packages/engine/src/modules/designer-types.ts +++ b/packages/engine/src/modules/designer-types.ts @@ -2,17 +2,11 @@ import * as designerCabin from '@alilc/lowcode-designer'; // 这样做的目的是为了去除 Node / DocumentModel 等的值属性,仅保留类型属性 export type Node = designerCabin.Node; -export type ParentalNode = designerCabin.ParentalNode; export type DocumentModel = designerCabin.DocumentModel; export type RootNode = designerCabin.RootNode; export type EditingTarget = designerCabin.EditingTarget; export type SaveHandler = designerCabin.SaveHandler; export type ComponentMeta = designerCabin.ComponentMeta; export type SettingField = designerCabin.SettingField; -export type ILowCodePluginConfig = designerCabin.ILowCodePluginConfig; export type ILowCodePluginManager = designerCabin.ILowCodePluginManager; -export type ILowCodePluginContext = designerCabin.ILowCodePluginContext; -export type PluginPreference = designerCabin.PluginPreference; -export type PropsReducerContext = designerCabin.PropsReducerContext; -export type DragObjectType = designerCabin.DragObjectType; -export type DragNodeDataObject = designerCabin.DragNodeDataObject; \ No newline at end of file +export type PluginPreference = designerCabin.PluginPreference; \ No newline at end of file diff --git a/packages/engine/src/modules/editor-cabin.ts b/packages/engine/src/modules/editor-cabin.ts deleted file mode 100644 index 3f2874a10a..0000000000 --- a/packages/engine/src/modules/editor-cabin.ts +++ /dev/null @@ -1,15 +0,0 @@ -export { - Title, - Tip, - shallowIntl, - createIntl, - intl, - createSetterContent, - obx, - observable, - makeObservable, - untracked, - computed, - observer, - globalLocale, -} from '@alilc/lowcode-editor-core'; \ No newline at end of file diff --git a/packages/engine/src/modules/editor-types.ts b/packages/engine/src/modules/editor-types.ts deleted file mode 100644 index 1306712a74..0000000000 --- a/packages/engine/src/modules/editor-types.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as editorCabin from '@alilc/lowcode-editor-core'; - -export type RegisteredSetter = editorCabin.RegisteredSetter; diff --git a/packages/engine/src/modules/live-editing.ts b/packages/engine/src/modules/live-editing.ts index 620fef895a..f1f32b88f7 100644 --- a/packages/engine/src/modules/live-editing.ts +++ b/packages/engine/src/modules/live-editing.ts @@ -1,5 +1,5 @@ import { EditingTarget, Node as DocNode, SaveHandler, LiveEditing } from '@alilc/lowcode-designer'; -import { isJSExpression } from '@alilc/lowcode-types'; +import { isJSExpression } from '@alilc/lowcode-utils'; function getText(node: DocNode, prop: string) { const p = node.getProp(prop, false); @@ -25,12 +25,12 @@ export function liveEditingRule(target: EditingTarget) { const targetElement = event.target as HTMLElement; - if (!Array.from(targetElement.childNodes).every(item => item.nodeType === Node.TEXT_NODE)) { + if (!Array.from(targetElement.childNodes).every((item) => item.nodeType === Node.TEXT_NODE)) { return null; } const { innerText } = targetElement; - const propTarget = ['title', 'label', 'text', 'content', 'children'].find(prop => { + const propTarget = ['title', 'label', 'text', 'content', 'children'].find((prop) => { return equalText(getText(node, prop), innerText); }); @@ -53,8 +53,7 @@ function equalText(v: any, innerText: string) { export const liveEditingSaveHander: SaveHandler = { condition: (prop) => { - // const v = prop.getValue(); - return prop.type === 'expression'; // || isI18nData(v); + return prop.type === 'expression'; }, onSaveContent: (content, prop) => { const v = prop.getValue(); diff --git a/packages/engine/src/modules/lowcode-types.ts b/packages/engine/src/modules/lowcode-types.ts index a39c24025e..50618bd31d 100644 --- a/packages/engine/src/modules/lowcode-types.ts +++ b/packages/engine/src/modules/lowcode-types.ts @@ -1 +1 @@ -export type { NodeSchema } from '@alilc/lowcode-types'; \ No newline at end of file +export type { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; \ No newline at end of file diff --git a/packages/engine/src/modules/shell-model-factory.ts b/packages/engine/src/modules/shell-model-factory.ts new file mode 100644 index 0000000000..4271d126a3 --- /dev/null +++ b/packages/engine/src/modules/shell-model-factory.ts @@ -0,0 +1,19 @@ +import { + INode, + ISettingField, +} from '@alilc/lowcode-designer'; +import { IShellModelFactory, IPublicModelNode } from '@alilc/lowcode-types'; +import { IPublicModelSettingField } from '../../../types/src/shell/model/setting-field'; +import { + Node, + SettingField, +} from '@alilc/lowcode-shell'; +class ShellModelFactory implements IShellModelFactory { + createNode(node: INode | null | undefined): IPublicModelNode | null { + return Node.create(node); + } + createSettingField(prop: ISettingField): IPublicModelSettingField { + return SettingField.create(prop); + } +} +export const shellModelFactory = new ShellModelFactory(); \ No newline at end of file diff --git a/packages/engine/src/modules/skeleton-cabin.tsx b/packages/engine/src/modules/skeleton-cabin.tsx deleted file mode 100644 index 1d8363ab1b..0000000000 --- a/packages/engine/src/modules/skeleton-cabin.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { - Skeleton as InnerSkeleton, - createSettingFieldView, - PopupContext, - PopupPipe, - Workbench as InnerWorkbench, -} from '@alilc/lowcode-editor-skeleton'; - -export default function getSkeletonCabin(skeleton: InnerSkeleton) { - return { - createSettingFieldView, - PopupContext, - PopupPipe, - Workbench: (props: any) => <InnerWorkbench {...props} skeleton={skeleton} />, // hijack skeleton - }; -} \ No newline at end of file diff --git a/packages/engine/src/modules/skeleton-types.ts b/packages/engine/src/modules/skeleton-types.ts index da17a4649b..8cce1c08cc 100644 --- a/packages/engine/src/modules/skeleton-types.ts +++ b/packages/engine/src/modules/skeleton-types.ts @@ -1,3 +1,3 @@ -import * as skeletonCabin from '@alilc/lowcode-editor-skeleton'; +import { IPublicTypeWidgetBaseConfig as innerIWidgetBaseConfig } from '@alilc/lowcode-types'; -export type IWidgetBaseConfig = skeletonCabin.IWidgetBaseConfig; +export type IWidgetBaseConfig = innerIWidgetBaseConfig; diff --git a/packages/engine/src/modules/symbols.ts b/packages/engine/src/modules/symbols.ts index f40e81654d..55c70e5dcb 100644 --- a/packages/engine/src/modules/symbols.ts +++ b/packages/engine/src/modules/symbols.ts @@ -6,8 +6,15 @@ import { designerSymbol, skeletonSymbol, editorSymbol, - settingPropEntrySymbol, + settingFieldSymbol, settingTopEntrySymbol, + designerCabinSymbol, + propSymbol, + simulatorHostSymbol, + skeletonItemSymbol, + editorCabinSymbol, + skeletonCabinSymbol, + simulatorRenderSymbol, } from '@alilc/lowcode-shell'; export default { @@ -18,6 +25,13 @@ export default { skeletonSymbol, editorSymbol, designerSymbol, - settingPropEntrySymbol, + settingPropEntrySymbol: settingFieldSymbol, settingTopEntrySymbol, + designerCabinSymbol, + editorCabinSymbol, + skeletonCabinSymbol, + propSymbol, + simulatorHostSymbol, + skeletonItemSymbol, + simulatorRenderSymbol, }; diff --git a/packages/engine/src/modules/utils.ts b/packages/engine/src/modules/utils.ts deleted file mode 100644 index 6fa569c75a..0000000000 --- a/packages/engine/src/modules/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isFormEvent, compatibleLegaoSchema, getNodeSchemaById } from '@alilc/lowcode-utils'; -import { isNodeSchema } from '@alilc/lowcode-types'; -import { getConvertedExtraKey, getOriginalExtraKey } from '@alilc/lowcode-designer'; - -const utils = { - isNodeSchema, - isFormEvent, - compatibleLegaoSchema, - getNodeSchemaById, - getConvertedExtraKey, - getOriginalExtraKey, -}; - -export default utils; \ No newline at end of file diff --git a/packages/ignitor/babel.config.js b/packages/ignitor/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/packages/ignitor/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/ignitor/build.json b/packages/ignitor/build.json index a29eaf0266..f1956cf527 100644 --- a/packages/ignitor/build.json +++ b/packages/ignitor/build.json @@ -1,15 +1,14 @@ { "entry": { - "engine-core": "../engine/src/index.ts", - "react-simulator-renderer": "../react-simulator-renderer/src/index.ts", - "rax-simulator-renderer": "../rax-simulator-renderer/src/index.ts" + "AliLowCodeEngine": "../engine/src/index.ts", + "ReactSimulatorRenderer": "../react-simulator-renderer/src/index.ts" }, "vendor": false, "devServer": { "liveReload": false, "hot": false }, - "library": "AliLowCodeEngine", + "library": "[name]", "publicPath": "/", "externals": { "react": "var window.React", diff --git a/packages/ignitor/jest.config.js b/packages/ignitor/jest.config.js new file mode 100644 index 0000000000..788c0ac79a --- /dev/null +++ b/packages/ignitor/jest.config.js @@ -0,0 +1,47 @@ +const fs = require('fs'); +const { join } = require('path'); +const esModules = [].join('|'); +const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); + +const jestConfig = { + // transform: { + // '^.+\\.[jt]sx?$': 'babel-jest', + // // '^.+\\.(ts|tsx)$': 'ts-jest', + // // '^.+\\.(js|jsx)$': 'babel-jest', + // }, + // testMatch: ['**/node-children.test.ts'], + // testMatch: ['**/plugin-manager.test.ts'], + // testMatch: ['**/history/history.test.ts'], + // testMatch: ['**/document-model.test.ts'], + // testMatch: ['**/prop.test.ts'], + // testMatch: ['(/tests?/.*(test))\\.[jt]s$'], + transformIgnorePatterns: [ + `/node_modules/(?!${esModules})/`, + ], + setupFiles: ['./tests/fixtures/unhandled-rejection.ts'], + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + collectCoverage: false, + collectCoverageFrom: [ + 'src/**/*.ts', + '!src/**/*.d.ts', + '!src/icons/**', + '!src/locale/**', + '!src/builtin-simulator/utils/**', + '!src/plugin/sequencify.ts', + '!src/document/node/exclusive-group.ts', + '!src/document/node/props/value-to-source.ts', + '!src/builtin-simulator/live-editing/live-editing.ts', + '!src/designer/offset-observer.ts', + '!src/designer/clipboard.ts', + '!src/designer/scroller.ts', + '!src/builtin-simulator/host.ts', + '!**/node_modules/**', + '!**/vendor/**', + ], +}; + +// 只对本仓库内的 pkg 做 mapping +jestConfig.moduleNameMapper = {}; +jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '<rootDir>/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/ignitor/package.json b/packages/ignitor/package.json index 73134cd0a4..0b109a7ad7 100644 --- a/packages/ignitor/package.json +++ b/packages/ignitor/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-ignitor", - "version": "1.0.15", + "version": "1.3.2", "description": "点火器,bootstrap lce project", "main": "lib/index.js", "private": true, @@ -16,5 +16,11 @@ "devDependencies": { "@alib/build-scripts": "^0.1.18", "fs-extra": "^10.0.0" - } + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/ignitor" + }, + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/ignitor/public/index.html b/packages/ignitor/public/index.html index 8f182f42c5..147fe334d9 100644 --- a/packages/ignitor/public/index.html +++ b/packages/ignitor/public/index.html @@ -8,12 +8,12 @@ </head> <body> <h1> - This project only provides engine resource files. For usage, go for + This project only provides engine resource files. For usage, go for <a href="https://github.com/alibaba/lowcode-demo" target="_blank">Lowcode Demo</a> </h1> <h2> For local debugging of lowcode engine, please visit - <a href="https://www.yuque.com/lce/doc/glz0fx#wi8rs" target="_blank">proxy documentation</a> + <a href="https://lowcode-engine.cn/site/docs/participate/prepare" target="_blank">proxy documentation</a> to get more information. </h2> </body> 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('|')})$`] = '<rootDir>/../$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 <liujup@foxmail.com>", + "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<string, any>; + }) { + 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/build.json b/packages/plugin-designer/build.json index bd5cf18dde..3e92600554 100644 --- a/packages/plugin-designer/build.json +++ b/packages/plugin-designer/build.json @@ -1,5 +1,5 @@ { "plugins": [ - "build-plugin-component" + "@alilc/build-plugin-lce" ] } diff --git a/packages/plugin-designer/package.json b/packages/plugin-designer/package.json index bfd937f314..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.0.15", + "version": "1.3.2", "description": "alibaba lowcode editor designer plugin", "files": [ "es", @@ -10,7 +10,7 @@ "module": "es/index.js", "stylePath": "style.js", "scripts": { - "build": "build-scripts build --skip-demo" + "build": "build-scripts build" }, "keywords": [ "lowcode", @@ -18,17 +18,16 @@ ], "author": "xiayang.xy", "dependencies": { - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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" }, "devDependencies": { "@alib/build-scripts": "^0.1.3", "@types/react": "^16.9.13", - "@types/react-dom": "^16.9.4", - "build-plugin-component": "^0.2.7" + "@types/react-dom": "^16.9.4" }, "publishConfig": { "access": "public", @@ -38,5 +37,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/plugin-designer" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/plugin-designer/src/index.tsx b/packages/plugin-designer/src/index.tsx index 59a2bd06db..51b81ff9d0 100644 --- a/packages/plugin-designer/src/index.tsx +++ b/packages/plugin-designer/src/index.tsx @@ -1,11 +1,13 @@ import React, { PureComponent } from 'react'; -import { Editor, engineConfig, globalContext } from '@alilc/lowcode-editor-core'; +import { Editor, engineConfig } from '@alilc/lowcode-editor-core'; import { DesignerView, Designer } from '@alilc/lowcode-designer'; -import { Asset } from '@alilc/lowcode-utils'; +import { Asset, getLogger } from '@alilc/lowcode-utils'; import './index.scss'; +const logger = getLogger({ level: 'warn', bizName: 'plugin:plugin-designer' }); + export interface PluginProps { - editor: Editor; + engineEditor: Editor; } interface DesignerPluginState { @@ -46,7 +48,7 @@ export default class DesignerPlugin extends PureComponent<PluginProps, DesignerP } private async setupAssets() { - const editor = globalContext.get('editor'); + const editor = this.props.engineEditor; try { const assets = await editor.onceGot('assets'); const renderEnv = engineConfig.get('renderEnv') || editor.get('renderEnv'); @@ -60,6 +62,21 @@ export default class DesignerPlugin extends PureComponent<PluginProps, DesignerP if (!this._mounted) { return; } + engineConfig.onGot('locale', (locale) => { + this.setState({ + locale, + }); + }); + engineConfig.onGot('requestHandlersMap', (requestHandlersMap) => { + this.setState({ + requestHandlersMap, + }); + }); + engineConfig.onGot('device', (device) => { + this.setState({ + device, + }); + }); const { components, packages, extraEnvironment, utils } = assets; const state = { componentMetadatas: components || [], @@ -76,7 +93,7 @@ export default class DesignerPlugin extends PureComponent<PluginProps, DesignerP }; this.setState(state); } catch (e) { - console.log(e); + logger.error(e); } } @@ -85,16 +102,16 @@ export default class DesignerPlugin extends PureComponent<PluginProps, DesignerP } private handleDesignerMount = (designer: Designer): void => { - const editor = globalContext.get('editor'); + const editor = this.props.engineEditor; editor.set('designer', designer); - editor.emit('designer.ready', designer); + editor.eventBus.emit('designer.ready', designer); editor.onGot('schema', (schema) => { designer.project.open(schema); }); }; render(): React.ReactNode { - const editor = globalContext.get('editor'); + const editor: Editor = this.props.engineEditor; const { componentMetadatas, utilsMetadata, @@ -119,6 +136,7 @@ export default class DesignerPlugin extends PureComponent<PluginProps, DesignerP onMount={this.handleDesignerMount} className="lowcode-plugin-designer" editor={editor} + name={editor.viewName} designer={editor.get('designer')} componentMetadatas={componentMetadatas} simulatorProps={{ diff --git a/packages/plugin-outline-pane/build.json b/packages/plugin-outline-pane/build.json index e791d5b6b3..d0aec10385 100644 --- a/packages/plugin-outline-pane/build.json +++ b/packages/plugin-outline-pane/build.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "build-plugin-fusion", ["build-plugin-moment-locales", { "locales": ["zh-cn"] diff --git a/packages/plugin-outline-pane/package.json b/packages/plugin-outline-pane/package.json index a763a50bee..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.0.15", + "version": "1.3.2", "description": "Outline pane for Ali lowCode engine", "files": [ "es", @@ -9,17 +9,16 @@ "main": "lib/index.js", "module": "es/index.js", "scripts": { - "build": "build-scripts build --skip-demo" + "build": "build-scripts build" }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@alilc/lowcode-types": "1.3.2", + "@alilc/lowcode-utils": "1.3.2", "classnames": "^2.2.6", "react": "^16", - "react-dom": "^16.7.0" + "react-dom": "^16.7.0", + "ric-shim": "^1.0.1" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", @@ -27,7 +26,6 @@ "@types/node": "^13.7.1", "@types/react": "^16", "@types/react-dom": "^16", - "build-plugin-component": "^0.2.10", "build-plugin-fusion": "^0.1.1", "build-plugin-moment-locales": "^0.1.0" }, @@ -40,5 +38,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/plugin-outline-pane" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/plugin-outline-pane/src/controllers/pane-controller.ts b/packages/plugin-outline-pane/src/controllers/pane-controller.ts new file mode 100644 index 0000000000..6a9ae7c40c --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/pane-controller.ts @@ -0,0 +1,669 @@ +/* eslint-disable max-len */ +import requestIdleCallback, { cancelIdleCallback } from 'ric-shim'; +import { + uniqueId, + isDragNodeObject, + isDragAnyObject, + isLocationChildrenDetail, +} from '@alilc/lowcode-utils'; +import { + IPublicModelDragObject, + IPublicTypeScrollable, + IPublicModelSensor, + IPublicTypeLocationChildrenDetail, + IPublicTypeLocationDetailType, + IPublicModelNode, + IPublicModelDropLocation, + IPublicModelScroller, + IPublicModelScrollTarget, + IPublicModelLocateEvent, +} from '@alilc/lowcode-types'; +import TreeNode from './tree-node'; +import { IndentTrack } from '../helper/indent-track'; +import DwellTimer from '../helper/dwell-timer'; +import { IOutlinePanelPluginContext, ITreeBoard, TreeMaster } from './tree-master'; + +export class PaneController implements IPublicModelSensor, ITreeBoard, IPublicTypeScrollable { + private pluginContext: IOutlinePanelPluginContext; + + private treeMaster?: TreeMaster; + + readonly id = uniqueId('outline'); + + private indentTrack = new IndentTrack(); + + private _sensorAvailable = false; + + /** + * @see IPublicModelSensor + */ + get sensorAvailable() { + return this._sensorAvailable; + } + + private dwell = new DwellTimer((target, event) => { + const { canvas, project } = this.pluginContext; + const document = project.getCurrentDocument(); + let index: any; + let focus: any; + let valid = true; + if (target.hasSlots()) { + index = null; + focus = { type: 'slots' }; + } else { + index = 0; + valid = !!document?.checkNesting(target, event.dragObject as any); + } + canvas.createLocation({ + target, + source: this.id, + event, + detail: { + type: IPublicTypeLocationDetailType.Children, + index, + focus, + valid, + }, + }); + }); + + /** + * @see ITreeBoard + */ + readonly at: string | symbol; + + private tryScrollAgain: number | null = null; + + private sensing = false; + + /** + * @see IScrollable + */ + get bounds(): DOMRect | null { + if (!this._shell) { + return null; + } + return this._shell.getBoundingClientRect(); + } + + private _scrollTarget?: IPublicModelScrollTarget; + + /** + * @see IScrollable + */ + get scrollTarget() { + return this._scrollTarget; + } + + private scroller?: IPublicModelScroller; + + private _shell: HTMLDivElement | null = null; + + constructor(at: string | symbol, treeMaster: TreeMaster) { + this.pluginContext = treeMaster.pluginContext; + this.treeMaster = treeMaster; + this.at = at; + let inited = false; + const setup = () => { + if (inited) { + return false; + } + inited = true; + this.treeMaster?.addBoard(this); + const { canvas } = this.pluginContext; + canvas.dragon?.addSensor(this); + this.scroller = canvas.createScroller(this); + }; + + setup(); + } + + /** -------------------- IPublicModelSensor begin -------------------- */ + + /** + * @see IPublicModelSensor + */ + fixEvent(e: IPublicModelLocateEvent): IPublicModelLocateEvent { + if (e.fixed) { + return e; + } + + const notMyEvent = e.originalEvent.view?.document !== document; + + if (!e.target || notMyEvent) { + e.target = document.elementFromPoint(e.canvasX!, e.canvasY!); + } + + // documentModel : 目标文档 + e.documentModel = this.pluginContext.project.getCurrentDocument(); + + // 事件已订正 + e.fixed = true; + return e; + } + + /** + * @see IPublicModelSensor + */ + locate(e: IPublicModelLocateEvent): IPublicModelDropLocation | undefined | null { + this.sensing = true; + this.scroller?.scrolling(e); + const { globalY, dragObject } = e; + const nodes = dragObject?.nodes; + + const tree = this.treeMaster?.currentTree; + if (!tree || !tree.root || !this._shell) { + return null; + } + + const operationalNodes = nodes?.filter((node: any) => { + const onMoveHook = node.componentMeta?.advanced.callbacks?.onMoveHook; + const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node) : true; + + return canMove; + }); + + // 如果拖拽的是 Node 才需要后面的判断,拖拽 data 不需要 + if (isDragNodeObject(dragObject) && (!operationalNodes || operationalNodes.length === 0)) { + return; + } + + const { project, canvas } = this.pluginContext; + const document = project.getCurrentDocument(); + const pos = getPosFromEvent(e, this._shell); + const irect = this.getInsertionRect(); + const originLoc = document?.dropLocation; + + const componentMeta = e.dragObject?.nodes ? e.dragObject.nodes[0].componentMeta : null; + if (e.dragObject?.type === 'node' && componentMeta && componentMeta.isModal && document?.focusNode) { + return canvas.createLocation({ + target: document?.focusNode, + detail: { + type: IPublicTypeLocationDetailType.Children, + index: 0, + valid: true, + }, + source: this.id, + event: e, + }); + } + + if (originLoc + && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom)) + && dragObject) { + const loc = originLoc.clone(e); + const indented = this.indentTrack.getIndentParent(originLoc, loc); + if (indented) { + const [parent, index] = indented; + if (checkRecursion(parent, dragObject)) { + if (tree.getTreeNode(parent).expanded) { + this.dwell.reset(); + return canvas.createLocation({ + target: parent, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index, + valid: document?.checkNesting(parent, e.dragObject as any), + }, + }); + } + + (originLoc.detail as IPublicTypeLocationChildrenDetail).focus = { + type: 'node', + node: parent, + }; + // focus try expand go on + this.dwell.focus(parent, e); + } else { + this.dwell.reset(); + } + // FIXME: recreate new location + } else if ((originLoc.detail as IPublicTypeLocationChildrenDetail).near) { + (originLoc.detail as IPublicTypeLocationChildrenDetail).near = undefined; + this.dwell.reset(); + } + return; + } + + this.indentTrack.reset(); + + if (pos && pos !== 'unchanged') { + let treeNode = tree.getTreeNodeById(pos.nodeId); + if (treeNode) { + let { focusSlots } = pos; + let { node } = treeNode; + if (isDragNodeObject(dragObject)) { + const newNodes = operationalNodes; + let i = newNodes?.length; + let p: any = node; + while (i-- > 0) { + if (newNodes[i].contains(p)) { + p = newNodes[i].parent; + } + } + if (p !== node) { + node = p || document?.focusNode; + treeNode = tree.getTreeNode(node); + focusSlots = false; + } + } + + if (focusSlots) { + this.dwell.reset(); + return canvas.createLocation({ + target: node as IPublicModelNode, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index: null, + valid: false, + focus: { type: 'slots' }, + }, + }); + } + + if (!treeNode.isRoot()) { + const loc = this.getNear(treeNode, e); + this.dwell.tryFocus(loc); + return loc; + } + } + } + + const loc = this.drillLocate(tree.root, e); + this.dwell.tryFocus(loc); + return loc; + } + + /** + * @see IPublicModelSensor + */ + isEnter(e: IPublicModelLocateEvent): boolean { + if (!this._shell) { + return false; + } + const rect = this._shell.getBoundingClientRect(); + return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right; + } + + /** + * @see IPublicModelSensor + */ + deactiveSensor() { + this.sensing = false; + this.scroller?.cancel(); + this.dwell.reset(); + this.indentTrack.reset(); + } + + /** -------------------- IPublicModelSensor end -------------------- */ + + /** -------------------- ITreeBoard begin -------------------- */ + + /** + * @see ITreeBoard + */ + scrollToNode(treeNode: TreeNode, detail?: any, tryTimes = 0) { + if (tryTimes < 1 && this.tryScrollAgain) { + cancelIdleCallback(this.tryScrollAgain); + this.tryScrollAgain = null; + } + if (!this.bounds || !this.scroller || !this.scrollTarget) { + // is a active sensor + return; + } + + let rect: ClientRect | undefined; + if (detail && isLocationChildrenDetail(detail)) { + rect = this.getInsertionRect(); + } else { + rect = this.getTreeNodeRect(treeNode); + } + + if (!rect) { + if (tryTimes < 3) { + this.tryScrollAgain = requestIdleCallback(() => this.scrollToNode(treeNode, detail, tryTimes + 1)); + } + return; + } + const { scrollHeight, top: scrollTop } = this.scrollTarget; + const { height, top, bottom } = this.bounds; + if (rect.top < top || rect.bottom > bottom) { + const opt: any = {}; + opt.top = Math.min(rect.top + rect.height / 2 + scrollTop - top - height / 2, scrollHeight - height); + if (rect.height >= height) { + opt.top = Math.min(scrollTop + rect.top - top, opt.top); + } + this.scroller.scrollTo(opt); + } + // make tail scroll be sure + if (tryTimes < 4) { + this.tryScrollAgain = requestIdleCallback(() => this.scrollToNode(treeNode, detail, 4)); + } + } + + /** -------------------- ITreeBoard end -------------------- */ + + private getNear(treeNode: TreeNode, e: IPublicModelLocateEvent, originalIndex?: number, originalRect?: DOMRect) { + const { canvas, project } = this.pluginContext; + const document = project.getCurrentDocument(); + const { globalY, dragObject } = e; + if (!dragObject) { + return null; + } + // TODO: check dragObject is anyData + const { node, expanded } = treeNode; + let rect = originalRect; + if (!rect) { + rect = this.getTreeNodeRect(treeNode); + if (!rect) { + return null; + } + } + let index = originalIndex; + if (index == null) { + index = node.index; + } + + if (node.isSlotNode) { + // 是个插槽根节点 + if (!treeNode.isContainer() && !treeNode.hasSlots()) { + return canvas.createLocation({ + target: node.parent!, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index: null, + near: { node, pos: 'replace' }, + valid: true, // TODO: future validation the slot limit + }, + }); + } + const loc1 = this.drillLocate(treeNode, e); + if (loc1) { + return loc1; + } + + return canvas.createLocation({ + target: node.parent!, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index: null, + valid: false, + focus: { type: 'slots' }, + }, + }); + } + + let focusNode: IPublicModelNode | undefined; + // focus + if (!expanded && (treeNode.isContainer() || treeNode.hasSlots())) { + focusNode = node; + } + + // before + const titleRect = this.getTreeTitleRect(treeNode) || rect; + if (globalY < titleRect.top + titleRect.height / 2) { + return canvas.createLocation({ + target: node.parent!, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index, + valid: document?.checkNesting(node.parent!, dragObject as any), + near: { node, pos: 'before' }, + focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, + }, + }); + } + + if (globalY > titleRect.bottom) { + focusNode = undefined; + } + + if (expanded) { + // drill + const loc = this.drillLocate(treeNode, e); + if (loc) { + return loc; + } + } + + // after + return canvas.createLocation({ + target: node.parent!, + source: this.id, + event: e, + detail: { + type: IPublicTypeLocationDetailType.Children, + index: (index || 0) + 1, + valid: document?.checkNesting(node.parent!, dragObject as any), + near: { node, pos: 'after' }, + focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, + }, + }); + } + + private drillLocate(treeNode: TreeNode, e: IPublicModelLocateEvent): IPublicModelDropLocation | null { + const { canvas, project } = this.pluginContext; + const document = project.getCurrentDocument(); + const { dragObject, globalY } = e; + if (!dragObject) { + return null; + } + + if (!checkRecursion(treeNode.node, dragObject)) { + return null; + } + + if (isDragAnyObject(dragObject)) { + // TODO: future + return null; + } + + const container = treeNode.node as IPublicModelNode; + const detail: IPublicTypeLocationChildrenDetail = { + type: IPublicTypeLocationDetailType.Children, + }; + const locationData: any = { + target: container, + detail, + source: this.id, + event: e, + }; + const isSlotContainer = treeNode.hasSlots(); + const isContainer = treeNode.isContainer(); + + if (container.isSlotNode && !treeNode.expanded) { + // 未展开,直接定位到内部第一个节点 + if (isSlotContainer) { + detail.index = null; + detail.focus = { type: 'slots' }; + detail.valid = false; + } else { + detail.index = 0; + detail.valid = document?.checkNesting(container, dragObject); + } + } + + let items: TreeNode[] | null = null; + let slotsRect: DOMRect | undefined; + let focusSlots = false; + // isSlotContainer + if (isSlotContainer) { + slotsRect = this.getTreeSlotsRect(treeNode); + if (slotsRect) { + if (globalY <= slotsRect.bottom) { + focusSlots = true; + items = treeNode.slots; + } else if (!isContainer) { + // 不在 slots 范围,又不是 container 的情况,高亮 slots 区 + detail.index = null; + detail.focus = { type: 'slots' }; + detail.valid = false; + return canvas.createLocation(locationData); + } + } + } + + if (!items && isContainer) { + items = treeNode.children; + } + + if (!items) { + return null; + } + + const l = items.length; + let index = 0; + let before = l < 1; + let current: TreeNode | undefined; + let currentIndex = index; + for (; index < l; index++) { + current = items[index]; + currentIndex = index; + const rect = this.getTreeNodeRect(current); + if (!rect) { + continue; + } + + // rect + if (globalY < rect.top) { + before = true; + break; + } + + if (globalY > rect.bottom) { + continue; + } + + const loc = this.getNear(current, e, index, rect); + if (loc) { + return loc; + } + } + + if (focusSlots) { + detail.focus = { type: 'slots' }; + detail.valid = false; + detail.index = null; + } else { + if (current) { + detail.index = before ? currentIndex : currentIndex + 1; + detail.near = { node: current.node, pos: before ? 'before' : 'after' }; + } else { + detail.index = l; + } + detail.valid = document?.checkNesting(container, dragObject); + } + + return canvas.createLocation(locationData); + } + + purge() { + const { canvas } = this.pluginContext; + canvas.dragon?.removeSensor(this); + this.treeMaster?.removeBoard(this); + } + + mount(shell: HTMLDivElement | null) { + if (this._shell === shell) { + return; + } + this._shell = shell; + const { canvas, project } = this.pluginContext; + if (shell) { + this._scrollTarget = canvas.createScrollTarget(shell); + this._sensorAvailable = true; + + // check if there is current selection and scroll to it + const selection = project.currentDocument?.selection; + const topNodes = selection?.getTopNodes(true); + const tree = this.treeMaster?.currentTree; + if (topNodes && topNodes[0] && tree) { + const treeNode = tree.getTreeNodeById(topNodes[0].id); + if (treeNode) { + // at this moment, it is possible that pane is not ready yet, so + // put ui related operations to the next loop + setTimeout(() => { + tree.setNodeSelected(treeNode.nodeId); + this.scrollToNode(treeNode, null, 4); + }, 0); + } + } + } else { + this._scrollTarget = undefined; + this._sensorAvailable = false; + } + } + + private getInsertionRect(): DOMRect | undefined { + if (!this._shell) { + return undefined; + } + return this._shell.querySelector('.insertion')?.getBoundingClientRect(); + } + + private getTreeNodeRect(treeNode: TreeNode): DOMRect | undefined { + if (!this._shell) { + return undefined; + } + return this._shell.querySelector(`.tree-node[data-id="${treeNode.nodeId}"]`)?.getBoundingClientRect(); + } + + private getTreeTitleRect(treeNode: TreeNode): DOMRect | undefined { + if (!this._shell) { + return undefined; + } + return this._shell.querySelector(`.tree-node-title[data-id="${treeNode.nodeId}"]`)?.getBoundingClientRect(); + } + + private getTreeSlotsRect(treeNode: TreeNode): DOMRect | undefined { + if (!this._shell) { + return undefined; + } + return this._shell.querySelector(`.tree-node-slots[data-id="${treeNode.nodeId}"]`)?.getBoundingClientRect(); + } +} + +function checkRecursion(parent: IPublicModelNode | undefined | null, dragObject: IPublicModelDragObject): boolean { + if (!parent) { + return false; + } + if (isDragNodeObject(dragObject)) { + const { nodes } = dragObject; + if (nodes.some((node: IPublicModelNode) => node.contains(parent))) { + return false; + } + } + return true; +} + +function getPosFromEvent( + { target }: IPublicModelLocateEvent, + stop: Element, +): null | 'unchanged' | { nodeId: string; focusSlots: boolean } { + if (!target || !stop.contains(target)) { + return null; + } + if (target.matches('.insertion')) { + return 'unchanged'; + } + const closest = target.closest('[data-id]'); + if (!closest || !stop.contains(closest)) { + return null; + } + + const nodeId = (closest as HTMLDivElement).dataset.id!; + return { + focusSlots: closest.matches('.tree-node-slots'), + nodeId, + }; +} diff --git a/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts b/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts new file mode 100644 index 0000000000..74f3fbd949 --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/ric-shim.d.ts @@ -0,0 +1 @@ +declare module 'ric-shim'; \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/controllers/tree-master.ts b/packages/plugin-outline-pane/src/controllers/tree-master.ts new file mode 100644 index 0000000000..f86ce3deca --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/tree-master.ts @@ -0,0 +1,184 @@ +import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; +import { IPublicModelPluginContext, IPublicTypeActiveTarget, IPublicModelNode, IPublicTypeDisposable, IPublicEnumPluginRegisterLevel } from '@alilc/lowcode-types'; +import TreeNode from './tree-node'; +import { Tree } from './tree'; +import EventEmitter from 'events'; +import { enUS, zhCN } from '../locale'; +import { ReactNode } from 'react'; + +export interface ITreeBoard { + readonly at: string | symbol; + scrollToNode(treeNode: TreeNode, detail?: any): void; +} + +enum EVENT_NAMES { + pluginContextChanged = 'pluginContextChanged', +} + +export interface IOutlinePanelPluginContext extends IPublicModelPluginContext { + extraTitle?: string; + intlNode(id: string, params?: object): ReactNode; + intl(id: string, params?: object): string; + getLocale(): string; +} + +export class TreeMaster { + pluginContext: IOutlinePanelPluginContext; + + private boards = new Set<ITreeBoard>(); + + private treeMap = new Map<string, Tree>(); + + private disposeEvents: (IPublicTypeDisposable | undefined)[] = []; + + event = new EventEmitter(); + + constructor(pluginContext: IPublicModelPluginContext, readonly options: { + extraTitle?: string; + }) { + this.setPluginContext(pluginContext); + const { workspace } = this.pluginContext; + this.initEvent(); + if (pluginContext.registerLevel === IPublicEnumPluginRegisterLevel.Workspace) { + this.setPluginContext(workspace.window?.currentEditorView); + let dispose: IPublicTypeDisposable | undefined; + const windowViewTypeChangeEvent = () => { + dispose = workspace.window?.onChangeViewType(() => { + this.setPluginContext(workspace.window?.currentEditorView); + }); + }; + + windowViewTypeChangeEvent(); + + workspace.onChangeActiveWindow(() => { + this.setPluginContext(workspace.window?.currentEditorView); + dispose && dispose(); + windowViewTypeChangeEvent(); + }); + } + } + + private setPluginContext(pluginContext: IPublicModelPluginContext | undefined | null) { + if (!pluginContext) { + return; + } + const { intl, intlNode, getLocale } = pluginContext.common.utils.createIntl({ + 'en-US': enUS, + 'zh-CN': zhCN, + }); + let _pluginContext: IOutlinePanelPluginContext = Object.assign(pluginContext, { + intl, + intlNode, + getLocale, + }); + _pluginContext.extraTitle = this.options && this.options['extraTitle']; + this.pluginContext = _pluginContext; + this.disposeEvent(); + this.initEvent(); + this.emitPluginContextChange(); + } + + private disposeEvent() { + this.disposeEvents.forEach(d => { + d && d(); + }); + } + + private initEvent() { + let startTime: any; + const { event, project, canvas } = this.pluginContext; + const setExpandByActiveTracker = (target: IPublicTypeActiveTarget) => { + const { node, detail } = target; + const tree = this.currentTree; + if (!tree/* || node.document !== tree.document */) { + return; + } + const treeNode = tree.getTreeNode(node); + if (detail && isLocationChildrenDetail(detail)) { + treeNode.expand(true); + } else { + treeNode.expandParents(); + } + this.boards.forEach((board) => { + board.scrollToNode(treeNode, detail); + }); + }; + this.disposeEvents = [ + canvas.dragon?.onDragstart(() => { + startTime = Date.now() / 1000; + // needs? + this.toVision(); + }), + canvas.activeTracker?.onChange(setExpandByActiveTracker), + canvas.dragon?.onDragend(() => { + const endTime: any = Date.now() / 1000; + const nodes = project.currentDocument?.selection?.getNodes(); + event.emit('outlinePane.dragend', { + selected: nodes + ?.map((n) => { + if (!n) { + return; + } + const npm = n?.componentMeta?.npm; + return ( + [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || n?.componentMeta?.componentName + ); + }) + .join('&'), + time: (endTime - startTime).toFixed(2), + }); + }), + project.onRemoveDocument((data: {id: string}) => { + const { id } = data; + this.treeMap.delete(id); + }), + ]; + if (canvas.activeTracker?.target) { + setExpandByActiveTracker(canvas.activeTracker?.target); + } + } + + private toVision() { + const tree = this.currentTree; + if (tree) { + const selection = this.pluginContext.project.getCurrentDocument()?.selection; + selection?.getTopNodes().forEach((node: IPublicModelNode) => { + tree.getTreeNode(node).setExpanded(false); + }); + } + } + + addBoard(board: ITreeBoard) { + this.boards.add(board); + } + + removeBoard(board: ITreeBoard) { + this.boards.delete(board); + } + + purge() { + // todo others purge + } + + onPluginContextChange(fn: () => void) { + this.event.on(EVENT_NAMES.pluginContextChanged, fn); + } + + emitPluginContextChange() { + this.event.emit(EVENT_NAMES.pluginContextChanged); + } + + get currentTree(): Tree | null { + const doc = this.pluginContext.project.getCurrentDocument(); + if (doc) { + const { id } = doc; + if (this.treeMap.has(id)) { + return this.treeMap.get(id)!; + } + const tree = new Tree(this); + this.treeMap.set(id, tree); + return tree; + } + return null; + } +} diff --git a/packages/plugin-outline-pane/src/controllers/tree-node.ts b/packages/plugin-outline-pane/src/controllers/tree-node.ts new file mode 100644 index 0000000000..34d06fee0b --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/tree-node.ts @@ -0,0 +1,366 @@ +import { + IPublicTypeTitleContent, + IPublicTypeLocationChildrenDetail, + IPublicModelNode, + IPublicTypeDisposable, +} from '@alilc/lowcode-types'; +import { isI18nData, isLocationChildrenDetail, uniqueId } from '@alilc/lowcode-utils'; +import EventEmitter from 'events'; +import { Tree } from './tree'; +import { IOutlinePanelPluginContext } from './tree-master'; + +/** + * 大纲树过滤结果 + */ +export interface FilterResult { + // 过滤条件是否生效 + filterWorking: boolean; + // 命中子节点 + matchChild: boolean; + // 命中本节点 + matchSelf: boolean; + // 关键字 + keywords: string; +} + +enum EVENT_NAMES { + filterResultChanged = 'filterResultChanged', + + expandedChanged = 'expandedChanged', + + hiddenChanged = 'hiddenChanged', + + lockedChanged = 'lockedChanged', + + titleLabelChanged = 'titleLabelChanged', + + expandableChanged = 'expandableChanged', + + conditionChanged = 'conditionChanged', +} + +export default class TreeNode { + readonly pluginContext: IOutlinePanelPluginContext; + event = new EventEmitter(); + + private _node: IPublicModelNode; + + readonly tree: Tree; + + private _filterResult: FilterResult = { + filterWorking: false, + matchChild: false, + matchSelf: false, + keywords: '', + }; + + /** + * 默认为折叠状态 + * 在初始化根节点时,设置为展开状态 + */ + private _expanded = false; + + id = uniqueId('treeNode'); + + get nodeId(): string { + return this.node.id; + } + + /** + * 是否可以展开 + */ + get expandable(): boolean { + if (this.locked) return false; + return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null; + } + + get expanded(): boolean { + return this.isRoot(true) || (this.expandable && this._expanded); + } + + /** + * 插入"线"位置信息 + */ + get dropDetail(): IPublicTypeLocationChildrenDetail | undefined | null { + const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; + return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail : null; + } + + get depth(): number { + return this.node.zLevel; + } + + get detecting() { + const doc = this.pluginContext.project.currentDocument; + return !!(doc?.isDetectingNode(this.node)); + } + + get hidden(): boolean { + const cv = this.node.isConditionalVisible(); + if (cv == null) { + return !this.node.visible; + } + return !cv; + } + + get locked(): boolean { + return this.node.isLocked; + } + + get selected(): boolean { + // TODO: check is dragging + const selection = this.pluginContext.project.getCurrentDocument()?.selection; + if (!selection) { + return false; + } + return selection?.has(this.node.id); + } + + get title(): IPublicTypeTitleContent { + return this.node.title; + } + + get titleLabel() { + let { title } = this; + if (!title) { + return ''; + } + if ((title as any).label) { + title = (title as any).label; + } + if (typeof title === 'string') { + return title; + } + if (isI18nData(title)) { + const currentLocale = this.pluginContext.getLocale(); + const currentTitle = title[currentLocale]; + return currentTitle; + } + return this.node.componentName; + } + + get icon() { + return this.node.componentMeta?.icon; + } + + get parent(): TreeNode | null { + const { parent } = this.node; + if (parent) { + return this.tree.getTreeNode(parent); + } + return null; + } + + get slots(): TreeNode[] { + // todo: shallowEqual + return this.node.slots.map((node) => this.tree.getTreeNode(node)); + } + + get condition(): boolean { + return this.node.hasCondition() && !this.node.conditionGroup; + } + + get children(): TreeNode[] | null { + return this.node.children?.map((node) => this.tree.getTreeNode(node)) || null; + } + + get node(): IPublicModelNode { + return this._node; + } + + constructor(tree: Tree, node: IPublicModelNode) { + this.tree = tree; + this.pluginContext = tree.pluginContext; + this._node = node; + } + + setLocked(flag: boolean) { + this.node.lock(flag); + this.event.emit(EVENT_NAMES.lockedChanged, flag); + } + deleteNode(node: IPublicModelNode) { + node && node.remove(); + } + onFilterResultChanged(fn: () => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.filterResultChanged, fn); + return () => { + this.event.off(EVENT_NAMES.filterResultChanged, fn); + }; + } + onExpandedChanged(fn: (expanded: boolean) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.expandedChanged, fn); + return () => { + this.event.off(EVENT_NAMES.expandedChanged, fn); + }; + } + onHiddenChanged(fn: (hidden: boolean) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.hiddenChanged, fn); + return () => { + this.event.off(EVENT_NAMES.hiddenChanged, fn); + }; + } + onLockedChanged(fn: (locked: boolean) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.lockedChanged, fn); + return () => { + this.event.off(EVENT_NAMES.lockedChanged, fn); + }; + } + + onTitleLabelChanged(fn: (treeNode: TreeNode) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.titleLabelChanged, fn); + + return () => { + this.event.off(EVENT_NAMES.titleLabelChanged, fn); + }; + } + + onConditionChanged(fn: (treeNode: TreeNode) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.conditionChanged, fn); + + return () => { + this.event.off(EVENT_NAMES.conditionChanged, fn); + }; + } + + onExpandableChanged(fn: (expandable: boolean) => void): IPublicTypeDisposable { + this.event.on(EVENT_NAMES.expandableChanged, fn); + return () => { + this.event.off(EVENT_NAMES.expandableChanged, fn); + }; + } + + /** + * 触发 onExpandableChanged 回调 + */ + notifyExpandableChanged(): void { + this.event.emit(EVENT_NAMES.expandableChanged, this.expandable); + } + + notifyTitleLabelChanged(): void { + this.event.emit(EVENT_NAMES.titleLabelChanged, this.title); + } + + notifyConditionChanged(): void { + this.event.emit(EVENT_NAMES.conditionChanged, this.condition); + } + + setHidden(flag: boolean) { + if (this.node.conditionGroup) { + return; + } + if (this.node.visible !== !flag) { + this.node.visible = !flag; + } + this.event.emit(EVENT_NAMES.hiddenChanged, flag); + } + + isFocusingNode(): boolean { + const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; + if (!loc) { + return false; + } + return ( + isLocationChildrenDetail(loc.detail) && loc.detail.focus?.type === 'node' && loc.detail?.focus?.node.id === this.nodeId + ); + } + + setExpanded(value: boolean) { + this._expanded = value; + this.event.emit(EVENT_NAMES.expandedChanged, value); + } + + isRoot(includeOriginalRoot = false) { + const rootNode = this.pluginContext.project.getCurrentDocument()?.root; + return this.tree.root === this || (includeOriginalRoot && rootNode === this.node); + } + + /** + * 是否是响应投放区 + */ + isResponseDropping(): boolean { + const loc = this.pluginContext.project.getCurrentDocument()?.dropLocation; + if (!loc) { + return false; + } + return loc.target?.id === this.nodeId; + } + + setTitleLabel(label: string) { + const origLabel = this.titleLabel; + if (label === origLabel) { + return; + } + if (label === '') { + this.node.getExtraProp('title', false)?.remove(); + } else { + this.node.getExtraProp('title', true)?.setValue(label); + } + this.event.emit(EVENT_NAMES.titleLabelChanged, this); + } + + /** + * 是否是容器,允许子节点拖入 + */ + isContainer(): boolean { + return this.node.isContainerNode; + } + + /** + * 判断是否有"插槽" + */ + hasSlots(): boolean { + return this.node.hasSlots(); + } + + hasChildren(): boolean { + return !!(this.isContainer() && this.node.children?.notEmptyNode); + } + + select(isMulti: boolean) { + const { node } = this; + + const selection = this.pluginContext.project.getCurrentDocument()?.selection; + if (isMulti) { + selection?.add(node.id); + } else { + selection?.select(node.id); + } + } + + /** + * 展开节点,支持依次展开父节点 + */ + expand(tryExpandParents = false) { + // 这边不能直接使用 expanded,需要额外判断是否可以展开 + // 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开 + if (this.expandable && !this._expanded) { + this.setExpanded(true); + } + if (tryExpandParents) { + this.expandParents(); + } + } + + expandParents() { + let p = this.node.parent; + while (p) { + this.tree.getTreeNode(p).setExpanded(true); + p = p.parent; + } + } + + setNode(node: IPublicModelNode) { + if (this._node !== node) { + this._node = node; + } + } + + get filterReult(): FilterResult { + return this._filterResult; + } + + setFilterReult(val: FilterResult) { + this._filterResult = val; + this.event.emit(EVENT_NAMES.filterResultChanged); + } +} diff --git a/packages/plugin-outline-pane/src/controllers/tree.ts b/packages/plugin-outline-pane/src/controllers/tree.ts new file mode 100644 index 0000000000..ca5a43c554 --- /dev/null +++ b/packages/plugin-outline-pane/src/controllers/tree.ts @@ -0,0 +1,130 @@ +import TreeNode from './tree-node'; +import { IPublicModelNode, IPublicTypePropChangeOptions } from '@alilc/lowcode-types'; +import { IOutlinePanelPluginContext, TreeMaster } from './tree-master'; + +export class Tree { + private treeNodesMap = new Map<string, TreeNode>(); + + readonly id: string | undefined; + + readonly pluginContext: IOutlinePanelPluginContext; + + get root(): TreeNode | null { + if (this.pluginContext.project.currentDocument?.focusNode) { + return this.getTreeNode(this.pluginContext.project.currentDocument.focusNode!); + } + return null; + } + + readonly treeMaster: TreeMaster; + + constructor(treeMaster: TreeMaster) { + this.treeMaster = treeMaster; + this.pluginContext = treeMaster.pluginContext; + const doc = this.pluginContext.project.currentDocument; + this.id = doc?.id; + + doc?.onChangeNodeChildren((info: {node: IPublicModelNode }) => { + const { node } = info; + const treeNode = this.getTreeNodeById(node.id); + treeNode?.notifyExpandableChanged(); + }); + + doc?.history.onChangeCursor(() => { + this.root?.notifyExpandableChanged(); + }); + + doc?.onChangeNodeProp((info: IPublicTypePropChangeOptions) => { + const { node, key } = info; + if (key === '___title___') { + const treeNode = this.getTreeNodeById(node.id); + treeNode?.notifyTitleLabelChanged(); + } else if (key === '___condition___') { + const treeNode = this.getTreeNodeById(node.id); + treeNode?.notifyConditionChanged(); + } + }); + + doc?.onChangeNodeVisible((node: IPublicModelNode, visible: boolean) => { + const treeNode = this.getTreeNodeById(node.id); + treeNode?.setHidden(!visible); + }); + + doc?.onImportSchema(() => { + this.treeNodesMap = new Map<string, TreeNode>(); + }); + } + + setNodeSelected(nodeId: string): void { + // 目标节点选中,其他节点展开 + const treeNode = this.treeNodesMap.get(nodeId); + if (!treeNode) { + return; + } + this.expandAllAncestors(treeNode); + } + + getTreeNode(node: IPublicModelNode): TreeNode { + if (this.treeNodesMap.has(node.id)) { + const tnode = this.treeNodesMap.get(node.id)!; + tnode.setNode(node); + return tnode; + } + + const treeNode = new TreeNode(this, node); + this.treeNodesMap.set(node.id, treeNode); + return treeNode; + } + + getTreeNodeById(id: string) { + return this.treeNodesMap.get(id); + } + + expandAllAncestors(treeNode: TreeNode | undefined | null) { + if (!treeNode) { + return; + } + if (treeNode.isRoot()) { + return; + } + const ancestors = []; + let currentNode: TreeNode | null | undefined = treeNode; + while (!treeNode.isRoot()) { + currentNode = currentNode?.parent; + if (currentNode) { + ancestors.unshift(currentNode); + } else { + break; + } + } + ancestors.forEach((ancestor) => { + ancestor.setExpanded(true); + }); + } + + expandAllDecendants(treeNode: TreeNode | undefined | null) { + if (!treeNode) { + return; + } + treeNode.setExpanded(true); + const children = treeNode && treeNode.children; + if (children) { + children.forEach((child) => { + this.expandAllDecendants(child); + }); + } + } + + collapseAllDecendants(treeNode: TreeNode | undefined | null): void { + if (!treeNode) { + return; + } + treeNode.setExpanded(false); + const children = treeNode && treeNode.children; + if (children) { + children.forEach((child) => { + this.collapseAllDecendants(child); + }); + } + } +} diff --git a/packages/plugin-outline-pane/src/helper/consts.ts b/packages/plugin-outline-pane/src/helper/consts.ts new file mode 100644 index 0000000000..aa6395d207 --- /dev/null +++ b/packages/plugin-outline-pane/src/helper/consts.ts @@ -0,0 +1,2 @@ +export const BackupPaneName = 'outline-backup-pane'; +export const MasterPaneName = 'outline-master-pane'; \ No newline at end of file diff --git a/packages/plugin-outline-pane/src/helper/dwell-timer.ts b/packages/plugin-outline-pane/src/helper/dwell-timer.ts index d004bce615..c35dc0d117 100644 --- a/packages/plugin-outline-pane/src/helper/dwell-timer.ts +++ b/packages/plugin-outline-pane/src/helper/dwell-timer.ts @@ -1,4 +1,6 @@ -import { ParentalNode, DropLocation, isLocationChildrenDetail, LocateEvent } from '@alilc/lowcode-designer'; +import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; +import { IPublicModelNode, IPublicModelDropLocation, IPublicModelLocateEvent } from '@alilc/lowcode-types'; + /** * 停留检查计时器 @@ -6,20 +8,20 @@ import { ParentalNode, DropLocation, isLocationChildrenDetail, LocateEvent } fro export default class DwellTimer { private timer: number | undefined; - private previous?: ParentalNode; + private previous?: IPublicModelNode; - private event?: LocateEvent; + private event?: IPublicModelLocateEvent; - private decide: (node: ParentalNode, event: LocateEvent) => void; + private decide: (node: IPublicModelNode, event: IPublicModelLocateEvent) => void; private timeout = 500; - constructor(decide: (node: ParentalNode, event: LocateEvent) => void, timeout = 500) { + constructor(decide: (node: IPublicModelNode, event: IPublicModelLocateEvent) => void, timeout = 500) { this.decide = decide; this.timeout = timeout; } - focus(node: ParentalNode, event: LocateEvent) { + focus(node: IPublicModelNode, event: IPublicModelLocateEvent) { this.event = event; if (this.previous === node) { return; @@ -32,7 +34,7 @@ export default class DwellTimer { }, this.timeout) as any; } - tryFocus(loc?: DropLocation | null) { + tryFocus(loc?: IPublicModelDropLocation | null) { if (!loc || !isLocationChildrenDetail(loc.detail)) { this.reset(); return; diff --git a/packages/plugin-outline-pane/src/helper/indent-track.ts b/packages/plugin-outline-pane/src/helper/indent-track.ts index c0b009e112..a9965bf291 100644 --- a/packages/plugin-outline-pane/src/helper/indent-track.ts +++ b/packages/plugin-outline-pane/src/helper/indent-track.ts @@ -1,4 +1,6 @@ -import { DropLocation, ParentalNode, isLocationChildrenDetail } from '@alilc/lowcode-designer'; +import { isLocationChildrenDetail } from '@alilc/lowcode-utils'; +import { IPublicModelDropLocation, IPublicModelNode } from '@alilc/lowcode-types'; + const IndentSensitive = 15; export class IndentTrack { @@ -8,7 +10,8 @@ export class IndentTrack { this.indentStart = null; } - getIndentParent(lastLoc: DropLocation, loc: DropLocation): [ParentalNode, number] | null { + // eslint-disable-next-line max-len + getIndentParent(lastLoc: IPublicModelDropLocation, loc: IPublicModelDropLocation): [IPublicModelNode, number | undefined] | null { if ( lastLoc.target !== loc.target || !isLocationChildrenDetail(lastLoc.detail) || @@ -31,11 +34,11 @@ export class IndentTrack { this.indentStart = loc.event.globalX; const direction = delta < 0 ? 'left' : 'right'; - let parent = loc.target; + let parent: IPublicModelNode = loc.target; const { index } = loc.detail; if (direction === 'left') { - if (!parent.parent || index < parent.children.size || parent.isSlot()) { + if (!parent.parent || index < (parent.children?.size || 0) || parent.isSlotNode) { return null; } return [(parent as any).parent, parent.index + 1]; @@ -43,9 +46,9 @@ export class IndentTrack { if (index === 0) { return null; } - parent = parent.children.get(index - 1) as any; - if (parent && parent.isContainer()) { - return [parent, parent.children.size]; + parent = parent.children?.get(index - 1) as any; + if (parent && parent.isContainerNode) { + return [parent, parent.children?.size]; } } diff --git a/packages/plugin-outline-pane/src/icons/delete.tsx b/packages/plugin-outline-pane/src/icons/delete.tsx new file mode 100644 index 0000000000..1f93600196 --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/delete.tsx @@ -0,0 +1,11 @@ +import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; + +export function IconDelete(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M224 256v639.84A64 64 0 0 0 287.84 960h448.32A64 64 0 0 0 800 895.84V256h64a32 32 0 1 0 0-64H160a32 32 0 1 0 0 64h64zM384 96c0-17.664 14.496-32 31.904-32h192.192C625.696 64 640 78.208 640 96c0 17.664-14.496 32-31.904 32H415.904A31.872 31.872 0 0 1 384 96z m-96 191.744C288 270.208 302.4 256 320.224 256h383.552C721.6 256 736 270.56 736 287.744v576.512C736 881.792 721.6 896 703.776 896H320.224A32.224 32.224 0 0 1 288 864.256V287.744zM352 352c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z m128 0c0-17.696 14.208-32.032 32-32.032 17.664 0 32 14.24 32 32v448c0 17.664-14.208 32-32 32-17.664 0-32-14.24-32-32V352z" /> + </SVGIcon> + ); +} + +IconDelete.displayName = 'IconDelete'; diff --git a/packages/plugin-outline-pane/src/icons/index.ts b/packages/plugin-outline-pane/src/icons/index.ts index 4681aeb298..d28f61dd28 100644 --- a/packages/plugin-outline-pane/src/icons/index.ts +++ b/packages/plugin-outline-pane/src/icons/index.ts @@ -1,2 +1,12 @@ export * from './lock'; export * from './unlock'; +export * from './arrow-right'; +export * from './cond'; +export * from './eye-close'; +export * from './eye'; +export * from './filter'; +export * from './loop'; +export * from './radio-active'; +export * from './radio'; +export * from './setting'; +export * from './delete'; diff --git a/packages/plugin-outline-pane/src/icons/setting.tsx b/packages/plugin-outline-pane/src/icons/setting.tsx new file mode 100644 index 0000000000..a42b0ee013 --- /dev/null +++ b/packages/plugin-outline-pane/src/icons/setting.tsx @@ -0,0 +1,12 @@ +import { SVGIcon, IconProps } from '@alilc/lowcode-utils'; + +export function IconSetting(props: IconProps) { + return ( + <SVGIcon viewBox="0 0 1024 1024" {...props}> + <path d="M965.824 405.952a180.48 180.48 0 0 1-117.12-85.376 174.464 174.464 0 0 1-16-142.08 22.208 22.208 0 0 0-7.04-23.552 480.576 480.576 0 0 0-153.6-89.216 23.104 23.104 0 0 0-24.32 5.76 182.208 182.208 0 0 1-135.68 57.92 182.208 182.208 0 0 1-133.12-56.64 23.104 23.104 0 0 0-26.88-7.04 478.656 478.656 0 0 0-153.6 89.856 22.208 22.208 0 0 0-7.04 23.552 174.464 174.464 0 0 1-16 141.44A180.48 180.48 0 0 1 58.24 405.952a22.4 22.4 0 0 0-17.28 17.792 455.08 455.08 0 0 0 0 176.512 22.4 22.4 0 0 0 17.28 17.792 180.48 180.48 0 0 1 117.12 84.736c25.408 42.944 31.232 94.592 16 142.08a22.208 22.208 0 0 0 7.04 23.552A480.576 480.576 0 0 0 352 957.632h7.68a23.04 23.04 0 0 0 16.64-7.04 184.128 184.128 0 0 1 266.944 0c6.592 8.96 18.752 11.968 28.8 7.04a479.36 479.36 0 0 0 156.16-88.576 22.208 22.208 0 0 0 7.04-23.552 174.464 174.464 0 0 1 13.44-142.72 180.48 180.48 0 0 1 117.12-84.736 22.4 22.4 0 0 0 17.28-17.792 452.613 452.613 0 0 0 0-176.512 23.04 23.04 0 0 0-17.28-17.792z m-42.88 169.408a218.752 218.752 0 0 0-128 98.112 211.904 211.904 0 0 0-21.76 156.736 415.936 415.936 0 0 1-112 63.68 217.472 217.472 0 0 0-149.12-63.68 221.312 221.312 0 0 0-149.12 63.68 414.592 414.592 0 0 1-112-63.68c12.8-53.12 4.288-109.12-23.68-156.096A218.752 218.752 0 0 0 101.12 575.36a386.176 386.176 0 0 1 0-127.36 218.752 218.752 0 0 0 128-98.112c27.2-47.552 34.944-103.68 21.76-156.8a415.296 415.296 0 0 1 112-63.68A221.44 221.44 0 0 0 512 187.392a218.24 218.24 0 0 0 149.12-57.984 413.952 413.952 0 0 1 112 63.744 211.904 211.904 0 0 0 23.04 156.096 218.752 218.752 0 0 0 128 98.112 386.65 386.65 0 0 1 0 127.36l-1.28 0.64z" /> + <path d="M512 320.576c-105.984 0-192 85.568-192 191.104a191.552 191.552 0 0 0 192 191.104c106.112 0 192.064-85.568 192.064-191.104a190.72 190.72 0 0 0-56.256-135.168 192.448 192.448 0 0 0-135.744-55.936z m0 318.528c-70.656 0-128-57.088-128-127.424 0-70.4 57.344-127.36 128-127.36 70.72 0 128 56.96 128 127.36 0 33.792-13.44 66.176-37.44 90.112a128.32 128.32 0 0 1-90.496 37.312z" /> + </SVGIcon> + ); +} + +IconSetting.displayName = 'IconSetting'; diff --git a/packages/plugin-outline-pane/src/index.ts b/packages/plugin-outline-pane/src/index.ts deleted file mode 100644 index 32161abb5f..0000000000 --- a/packages/plugin-outline-pane/src/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { OutlinePane } from './views/pane'; -import { OutlineBackupPane } from './views/backup-pane'; -import { IconOutline } from './icons/outline'; -import { intlNode } from './locale'; -import { getTreeMaster } from './tree-master'; - -export default { - name: 'outline-pane', - props: { - icon: IconOutline, - description: intlNode('Outline Tree'), - }, - content: OutlinePane, -}; - -export { OutlinePane, OutlineBackupPane, getTreeMaster }; diff --git a/packages/plugin-outline-pane/src/index.tsx b/packages/plugin-outline-pane/src/index.tsx new file mode 100644 index 0000000000..822c503f27 --- /dev/null +++ b/packages/plugin-outline-pane/src/index.tsx @@ -0,0 +1,168 @@ +import { Pane } from './views/pane'; +import { IconOutline } from './icons/outline'; +import { IPublicModelPluginContext, IPublicModelDocumentModel } from '@alilc/lowcode-types'; +import { MasterPaneName, BackupPaneName } from './helper/consts'; +import { TreeMaster } from './controllers/tree-master'; +import { PaneController } from './controllers/pane-controller'; +import { useState, useEffect } from 'react'; + +export function OutlinePaneContext(props: { + treeMaster?: TreeMaster; + + pluginContext: IPublicModelPluginContext; + + options: any; + + paneName: string; + + hideFilter?: boolean; +}) { + const treeMaster = props.treeMaster || new TreeMaster(props.pluginContext, props.options); + const [masterPaneController, setMasterPaneController] = useState( + () => new PaneController(props.paneName || MasterPaneName, treeMaster), + ); + useEffect(() => { + return treeMaster.onPluginContextChange(() => { + setMasterPaneController(new PaneController(props.paneName || MasterPaneName, treeMaster)); + }); + }, []); + + return ( + <Pane + treeMaster={treeMaster} + controller={masterPaneController} + key={masterPaneController.id} + hideFilter={props.hideFilter} + {...props} + /> + ); +} + +export const OutlinePlugin = (ctx: IPublicModelPluginContext, options: any) => { + const { skeleton, config, canvas, project } = ctx; + + let isInFloatArea = true; + const hasPreferenceForOutline = config + .getPreference() + .contains('outline-pane-pinned-status-isFloat', 'skeleton'); + if (hasPreferenceForOutline) { + isInFloatArea = config.getPreference().get('outline-pane-pinned-status-isFloat', 'skeleton'); + } + const showingPanes = { + masterPane: false, + backupPane: false, + }; + const treeMaster = new TreeMaster(ctx, options); + return { + async init() { + skeleton.add({ + area: 'leftArea', + name: 'outlinePane', + type: 'PanelDock', + index: -1, + content: { + name: MasterPaneName, + props: { + icon: IconOutline, + description: treeMaster.pluginContext.intlNode('Outline Tree'), + }, + content: OutlinePaneContext, + }, + panelProps: { + area: isInFloatArea ? 'leftFloatArea' : 'leftFixedArea', + keepVisibleWhileDragging: true, + ...config.get('defaultOutlinePaneProps'), + }, + contentProps: { + treeTitleExtra: config.get('treeTitleExtra'), + treeMaster, + paneName: MasterPaneName, + }, + }); + + skeleton.add({ + area: 'rightArea', + name: BackupPaneName, + type: 'Panel', + props: { + hiddenWhenInit: true, + }, + content: OutlinePaneContext, + contentProps: { + paneName: BackupPaneName, + treeMaster, + }, + index: 1, + }); + + // 处理 master pane 和 backup pane 切换 + const switchPanes = () => { + const isDragging = canvas.dragon?.dragging; + const hasVisibleTreeBoard = showingPanes.backupPane || showingPanes.masterPane; + const shouldShowBackupPane = isDragging && !hasVisibleTreeBoard; + + if (shouldShowBackupPane) { + skeleton.showPanel(BackupPaneName); + } else { + skeleton.hidePanel(BackupPaneName); + } + }; + canvas.dragon?.onDragstart(() => { + switchPanes(); + }); + canvas.dragon?.onDragend(() => { + switchPanes(); + }); + skeleton.onShowPanel((key: string) => { + if (key === MasterPaneName) { + showingPanes.masterPane = true; + } + if (key === BackupPaneName) { + showingPanes.backupPane = true; + } + }); + skeleton.onHidePanel((key: string) => { + if (key === MasterPaneName) { + showingPanes.masterPane = false; + switchPanes(); + } + if (key === BackupPaneName) { + showingPanes.backupPane = false; + } + }); + project.onChangeDocument((document: IPublicModelDocumentModel) => { + if (!document) { + return; + } + + const { selection } = document; + + selection?.onSelectionChange(() => { + const selectedNodes = selection?.getNodes(); + if (!selectedNodes || selectedNodes.length === 0) { + return; + } + const tree = treeMaster.currentTree; + selectedNodes.forEach((node) => { + const treeNode = tree?.getTreeNodeById(node.id); + tree?.expandAllAncestors(treeNode); + }); + }); + }); + }, + }; +}; +OutlinePlugin.meta = { + eventPrefix: 'OutlinePlugin', + preferenceDeclaration: { + title: '大纲树插件配置', + properties: [ + { + key: 'extraTitle', + type: 'object', + description: '副标题', + }, + ], + }, +}; +OutlinePlugin.pluginName = 'OutlinePlugin'; diff --git a/packages/plugin-outline-pane/src/locale/en-US.json b/packages/plugin-outline-pane/src/locale/en-US.json index 9d04defc03..b86a7859bb 100644 --- a/packages/plugin-outline-pane/src/locale/en-US.json +++ b/packages/plugin-outline-pane/src/locale/en-US.json @@ -10,5 +10,14 @@ "Loop": "Loop", "Slots": "Slots", "Slot for {prop}": "Slot for {prop}", - "Outline Tree": "Outline Tree" + "Outline Tree": "Component Tree", + "Filter Node": "Filter Node", + "Check All": "Check All", + "Conditional rendering": "Conditional rendering", + "Loop rendering": "Loop rendering", + "Locked": "Locked", + "Hidden": "Hidden", + "Modal View": "Modal View", + "Rename": "Rename", + "Delete": "Delete" } diff --git a/packages/plugin-outline-pane/src/locale/index.ts b/packages/plugin-outline-pane/src/locale/index.ts index a912240fa3..b61ab90401 100644 --- a/packages/plugin-outline-pane/src/locale/index.ts +++ b/packages/plugin-outline-pane/src/locale/index.ts @@ -1,10 +1,4 @@ -import { createIntl } from '@alilc/lowcode-editor-core'; -import en_US from './en-US.json'; -import zh_CN from './zh-CN.json'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; -const { intl, intlNode, getLocale, setLocale } = createIntl({ - 'en-US': en_US, - 'zh-CN': zh_CN, -}); - -export { intl, intlNode, getLocale, setLocale }; +export { enUS, zhCN }; diff --git a/packages/plugin-outline-pane/src/locale/zh-CN.json b/packages/plugin-outline-pane/src/locale/zh-CN.json index 08138b3b4d..9070d17715 100644 --- a/packages/plugin-outline-pane/src/locale/zh-CN.json +++ b/packages/plugin-outline-pane/src/locale/zh-CN.json @@ -10,5 +10,14 @@ "Loop": "循环", "Slots": "插槽", "Slot for {prop}": "属性 {prop} 的插槽", - "Outline Tree": "大纲树" + "Outline Tree": "大纲树", + "Filter Node": "过滤节点", + "Check All": "全选", + "Conditional rendering": "条件渲染", + "Loop rendering": "循环渲染", + "Locked": "已锁定", + "Hidden": "已隐藏", + "Modal View": "模态视图层", + "Rename": "重命名", + "Delete": "删除" } diff --git a/packages/plugin-outline-pane/src/main.ts b/packages/plugin-outline-pane/src/main.ts deleted file mode 100644 index 85c4021f01..0000000000 --- a/packages/plugin-outline-pane/src/main.ts +++ /dev/null @@ -1,669 +0,0 @@ -import { computed, makeObservable, obx } from '@alilc/lowcode-editor-core'; -import { - Designer, - ISensor, - LocateEvent, - isDragNodeObject, - isDragAnyObject, - DragObject, - Scroller, - ScrollTarget, - IScrollable, - DropLocation, - isLocationChildrenDetail, - LocationChildrenDetail, - LocationDetailType, - ParentalNode, - contains, - Node, -} from '@alilc/lowcode-designer'; -import { uniqueId } from '@alilc/lowcode-utils'; -import { IEditor } from '@alilc/lowcode-types'; -import TreeNode from './tree-node'; -import { IndentTrack } from './helper/indent-track'; -import DwellTimer from './helper/dwell-timer'; -import { Backup } from './views/backup-pane'; -import { ITreeBoard, TreeMaster, getTreeMaster } from './tree-master'; - -export class OutlineMain implements ISensor, ITreeBoard, IScrollable { - private _designer?: Designer; - - @obx.ref private _master?: TreeMaster; - - get master() { - return this._master; - } - - @computed get currentTree() { - return this._master?.currentTree; - } - - readonly id = uniqueId('outline'); - - @obx.ref _visible = false; - - get visible() { - return this._visible; - } - - readonly editor: IEditor; - - readonly at: string | symbol; - - constructor(editor: IEditor, at: string | symbol) { - makeObservable(this); - this.editor = editor; - this.at = at; - let inited = false; - const setup = async () => { - if (inited) { - return false; - } - inited = true; - const designer = await editor.onceGot('designer'); - this.setupDesigner(designer); - }; - - if (at === Backup) { - setup(); - } else { - editor.on('skeleton.panel.show', (key: string) => { - if (key === at) { - setup(); - this._visible = true; - } - }); - editor.on('skeleton.panel.hide', (key: string) => { - if (key === at) { - this._visible = false; - } - }); - } - } - - /** - * @see ISensor - */ - fixEvent(e: LocateEvent): LocateEvent { - if (e.fixed) { - return e; - } - - const notMyEvent = e.originalEvent.view?.document !== document; - - if (!e.target || notMyEvent) { - e.target = document.elementFromPoint(e.canvasX!, e.canvasY!); - } - - // documentModel : 目标文档 - e.documentModel = this._designer?.currentDocument; - - // 事件已订正 - e.fixed = true; - return e; - } - - private indentTrack = new IndentTrack(); - - private dwell = new DwellTimer((target, event) => { - const { document } = target; - const { designer } = document; - let index: any; - let focus: any; - let valid = true; - if (target.hasSlots()) { - index = null; - focus = { type: 'slots' }; - } else { - index = 0; - valid = document.checkNesting(target, event.dragObject as any); - } - designer.createLocation({ - target, - source: this.id, - event, - detail: { - type: LocationDetailType.Children, - index, - focus, - valid, - }, - }); - }); - - /** - * @see ISensor - */ - locate(e: LocateEvent): DropLocation | undefined | null { - this.sensing = true; - this.scroller?.scrolling(e); - const { globalY, dragObject } = e; - const { nodes } = dragObject; - - const tree = this._master?.currentTree; - if (!tree || !tree.root || !this._shell) { - return null; - } - - const operationalNodes = nodes?.filter((node: any) => { - const onMoveHook = node.componentMeta?.getMetadata().configure?.advanced?.callbacks?.onMoveHook; - const canMove = onMoveHook && typeof onMoveHook === 'function' ? onMoveHook(node) : true; - - return canMove; - }); - - // 如果拖拽的是 Node 才需要后面的判断,拖拽 data 不需要 - if (isDragNodeObject(dragObject) && (!operationalNodes || operationalNodes.length === 0)) { - return; - } - - const { document } = tree; - const { designer } = document; - const pos = getPosFromEvent(e, this._shell); - const irect = this.getInsertionRect(); - const originLoc = document.dropLocation; - - const componentMeta = e.dragObject.nodes ? e.dragObject.nodes[0].componentMeta : null; - if (e.dragObject.type === 'node' && componentMeta && componentMeta.isModal) { - return designer.createLocation({ - target: document.focusNode, - detail: { - type: LocationDetailType.Children, - index: 0, - valid: true, - }, - source: this.id, - event: e, - }); - } - - if (originLoc && ((pos && pos === 'unchanged') || (irect && globalY >= irect.top && globalY <= irect.bottom))) { - const loc = originLoc.clone(e); - const indented = this.indentTrack.getIndentParent(originLoc, loc); - if (indented) { - const [parent, index] = indented; - if (checkRecursion(parent, dragObject)) { - if (tree.getTreeNode(parent).expanded) { - this.dwell.reset(); - return designer.createLocation({ - target: parent, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index, - valid: document.checkNesting(parent, e.dragObject as any), - }, - }); - } - - (originLoc.detail as LocationChildrenDetail).focus = { - type: 'node', - node: parent, - }; - // focus try expand go on - this.dwell.focus(parent, e); - } else { - this.dwell.reset(); - } - // FIXME: recreate new location - } else if ((originLoc.detail as LocationChildrenDetail).near) { - (originLoc.detail as LocationChildrenDetail).near = undefined; - this.dwell.reset(); - } - return; - } - - this.indentTrack.reset(); - - if (pos && pos !== 'unchanged') { - let treeNode = tree.getTreeNodeById(pos.nodeId); - if (treeNode) { - let { focusSlots } = pos; - let { node } = treeNode; - if (isDragNodeObject(dragObject)) { - const newNodes = operationalNodes; - let i = newNodes.length; - let p: any = node; - while (i-- > 0) { - if (contains(newNodes[i], p)) { - p = newNodes[i].parent; - } - } - if (p !== node) { - node = p || document.focusNode; - treeNode = tree.getTreeNode(node); - focusSlots = false; - } - } - - if (focusSlots) { - this.dwell.reset(); - return designer.createLocation({ - target: node as ParentalNode, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index: null, - valid: false, - focus: { type: 'slots' }, - }, - }); - } - - if (!treeNode.isRoot()) { - const loc = this.getNear(treeNode, e); - this.dwell.tryFocus(loc); - return loc; - } - } - } - - const loc = this.drillLocate(tree.root, e); - this.dwell.tryFocus(loc); - return loc; - } - - private getNear(treeNode: TreeNode, e: LocateEvent, index?: number, rect?: DOMRect) { - const { document } = treeNode.tree; - const { designer } = document; - const { globalY, dragObject } = e; - // TODO: check dragObject is anyData - const { node, expanded } = treeNode; - if (!rect) { - rect = this.getTreeNodeRect(treeNode); - if (!rect) { - return null; - } - } - if (index == null) { - index = node.index; - } - - if (node.isSlot()) { - // 是个插槽根节点 - if (!treeNode.isContainer() && !treeNode.hasSlots()) { - return designer.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index: null, - near: { node, pos: 'replace' }, - valid: true, // TODO: future validation the slot limit - }, - }); - } - const loc1 = this.drillLocate(treeNode, e); - if (loc1) { - return loc1; - } - - return designer.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index: null, - valid: false, - focus: { type: 'slots' }, - }, - }); - } - - let focusNode: Node | undefined; - // focus - if (!expanded && (treeNode.isContainer() || treeNode.hasSlots())) { - focusNode = node; - } - - // before - const titleRect = this.getTreeTitleRect(treeNode) || rect; - if (globalY < titleRect.top + titleRect.height / 2) { - return designer.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index, - valid: document.checkNesting(node.parent!, dragObject as any), - near: { node, pos: 'before' }, - focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, - }, - }); - } - - if (globalY > titleRect.bottom) { - focusNode = undefined; - } - - if (expanded) { - // drill - const loc = this.drillLocate(treeNode, e); - if (loc) { - return loc; - } - } - - // after - return designer.createLocation({ - target: node.parent!, - source: this.id, - event: e, - detail: { - type: LocationDetailType.Children, - index: index + 1, - valid: document.checkNesting(node.parent!, dragObject as any), - near: { node, pos: 'after' }, - focus: checkRecursion(focusNode, dragObject) ? { type: 'node', node: focusNode } : undefined, - }, - }); - } - - private drillLocate(treeNode: TreeNode, e: LocateEvent): DropLocation | null { - const { document } = treeNode.tree; - const { designer } = document; - const { dragObject, globalY } = e; - - if (!checkRecursion(treeNode.node, dragObject)) { - return null; - } - - if (isDragAnyObject(dragObject)) { - // TODO: future - return null; - } - - const container = treeNode.node as ParentalNode; - const detail: LocationChildrenDetail = { - type: LocationDetailType.Children, - }; - const locationData: any = { - target: container, - detail, - source: this.id, - event: e, - }; - const isSlotContainer = treeNode.hasSlots(); - const isContainer = treeNode.isContainer(); - - if (container.isSlot() && !treeNode.expanded) { - // 未展开,直接定位到内部第一个节点 - if (isSlotContainer) { - detail.index = null; - detail.focus = { type: 'slots' }; - detail.valid = false; - } else { - detail.index = 0; - detail.valid = document.checkNesting(container, dragObject); - } - } - - let items: TreeNode[] | null = null; - let slotsRect: DOMRect | undefined; - let focusSlots = false; - // isSlotContainer - if (isSlotContainer) { - slotsRect = this.getTreeSlotsRect(treeNode); - if (slotsRect) { - if (globalY <= slotsRect.bottom) { - focusSlots = true; - items = treeNode.slots; - } else if (!isContainer) { - // 不在 slots 范围,又不是 container 的情况,高亮 slots 区 - detail.index = null; - detail.focus = { type: 'slots' }; - detail.valid = false; - return designer.createLocation(locationData); - } - } - } - - if (!items && isContainer) { - items = treeNode.children; - } - - if (!items) { - return null; - } - - const l = items.length; - let index = 0; - let before = l < 1; - let current: TreeNode | undefined; - let currentIndex = index; - for (; index < l; index++) { - current = items[index]; - currentIndex = index; - const rect = this.getTreeNodeRect(current); - if (!rect) { - continue; - } - - // rect - if (globalY < rect.top) { - before = true; - break; - } - - if (globalY > rect.bottom) { - continue; - } - - const loc = this.getNear(current, e, index, rect); - if (loc) { - return loc; - } - } - - if (focusSlots) { - detail.focus = { type: 'slots' }; - detail.valid = false; - detail.index = null; - } else { - if (current) { - detail.index = before ? currentIndex : currentIndex + 1; - detail.near = { node: current.node, pos: before ? 'before' : 'after' }; - } else { - detail.index = l; - } - detail.valid = document.checkNesting(container, dragObject); - } - - return designer.createLocation(locationData); - } - - /** - * @see ISensor - */ - isEnter(e: LocateEvent): boolean { - if (!this._shell) { - return false; - } - const rect = this._shell.getBoundingClientRect(); - return e.globalY >= rect.top && e.globalY <= rect.bottom && e.globalX >= rect.left && e.globalX <= rect.right; - } - - private tryScrollAgain: number | null = null; - - /** - * @see IScrollBoard - */ - scrollToNode(treeNode: TreeNode, detail?: any, tryTimes = 0) { - if (tryTimes < 1 && this.tryScrollAgain) { - (window as any).cancelIdleCallback(this.tryScrollAgain); - this.tryScrollAgain = null; - } - if (this.sensing || !this.bounds || !this.scroller || !this.scrollTarget) { - // is a active sensor - return; - } - - let rect: ClientRect | undefined; - if (detail && isLocationChildrenDetail(detail)) { - rect = this.getInsertionRect(); - } else { - rect = this.getTreeNodeRect(treeNode); - } - - if (!rect) { - if (tryTimes < 3) { - this.tryScrollAgain = (window as any).requestIdleCallback(() => this.scrollToNode(treeNode, detail, tryTimes + 1)); - } - return; - } - const { scrollHeight, top: scrollTop } = this.scrollTarget; - const { height, top, bottom } = this.bounds; - if (rect.top < top || rect.bottom > bottom) { - const opt: any = {}; - opt.top = Math.min(rect.top + rect.height / 2 + scrollTop - top - height / 2, scrollHeight - height); - if (rect.height >= height) { - opt.top = Math.min(scrollTop + rect.top - top, opt.top); - } - this.scroller.scrollTo(opt); - } - // make tail scroll be sure - if (tryTimes < 4) { - this.tryScrollAgain = (window as any).requestIdleCallback(() => this.scrollToNode(treeNode, detail, 4)); - } - } - - private sensing = false; - - /** - * @see ISensor - */ - deactiveSensor() { - this.sensing = false; - this.scroller?.cancel(); - this.dwell.reset(); - this.indentTrack.reset(); - } - - /** - * @see IScrollable - */ - get bounds(): DOMRect | null { - if (!this._shell) { - return null; - } - return this._shell.getBoundingClientRect(); - } - - private _scrollTarget?: ScrollTarget; - - /** - * @see IScrollable - */ - get scrollTarget() { - return this._scrollTarget; - } - - private scroller?: Scroller; - - private setupDesigner(designer: Designer) { - this._designer = designer; - this._master = getTreeMaster(designer); - this._master.addBoard(this); - designer.dragon.addSensor(this); - this.scroller = designer.createScroller(this); - } - - purge() { - this._designer?.dragon.removeSensor(this); - this._master?.removeBoard(this); - // todo purge treeMaster if needed - } - - private _sensorAvailable = false; - - /** - * @see ISensor - */ - get sensorAvailable() { - return this._sensorAvailable; - } - - private _shell: HTMLDivElement | null = null; - - mount(shell: HTMLDivElement | null) { - if (this._shell === shell) { - return; - } - this._shell = shell; - if (shell) { - this._scrollTarget = new ScrollTarget(shell); - this._sensorAvailable = true; - } else { - this._scrollTarget = undefined; - this._sensorAvailable = false; - } - } - - private getInsertionRect(): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell.querySelector('.insertion')?.getBoundingClientRect(); - } - - private getTreeNodeRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell.querySelector(`.tree-node[data-id="${treeNode.id}"]`)?.getBoundingClientRect(); - } - - private getTreeTitleRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell.querySelector(`.tree-node-title[data-id="${treeNode.id}"]`)?.getBoundingClientRect(); - } - - private getTreeSlotsRect(treeNode: TreeNode): DOMRect | undefined { - if (!this._shell) { - return undefined; - } - return this._shell.querySelector(`.tree-node-slots[data-id="${treeNode.id}"]`)?.getBoundingClientRect(); - } -} - -function checkRecursion(parent: Node | undefined | null, dragObject: DragObject): parent is ParentalNode { - if (!parent) { - return false; - } - if (isDragNodeObject(dragObject)) { - const { nodes } = dragObject; - if (nodes.some((node) => node.contains(parent))) { - return false; - } - } - return true; -} - -function getPosFromEvent( - { target }: LocateEvent, - stop: Element, -): null | 'unchanged' | { nodeId: string; focusSlots: boolean } { - if (!target || !stop.contains(target)) { - return null; - } - if (target.matches('.insertion')) { - return 'unchanged'; - } - target = target.closest('[data-id]'); - if (!target || !stop.contains(target)) { - return null; - } - - const nodeId = (target as HTMLDivElement).dataset.id!; - return { - focusSlots: target.matches('.tree-node-slots'), - nodeId, - }; -} diff --git a/packages/plugin-outline-pane/src/tree-master.ts b/packages/plugin-outline-pane/src/tree-master.ts deleted file mode 100644 index afed14f20f..0000000000 --- a/packages/plugin-outline-pane/src/tree-master.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { computed, makeObservable, obx } from '@alilc/lowcode-editor-core'; -import { Designer, isLocationChildrenDetail } from '@alilc/lowcode-designer'; -import TreeNode from './tree-node'; -import { Tree } from './tree'; -import { Backup } from './views/backup-pane'; - -export interface ITreeBoard { - readonly visible: boolean; - readonly at: string | symbol; - scrollToNode(treeNode: TreeNode, detail?: any): void; -} - -export class TreeMaster { - readonly designer: Designer; - - constructor(designer: Designer) { - makeObservable(this); - this.designer = designer; - let startTime: any; - designer.dragon.onDragstart(() => { - startTime = Date.now() / 1000; - // needs? - this.toVision(); - }); - designer.activeTracker.onChange(({ node, detail }) => { - const tree = this.currentTree; - if (!tree || node.document !== tree.document) { - return; - } - - const treeNode = tree.getTreeNode(node); - if (detail && isLocationChildrenDetail(detail)) { - treeNode.expand(true); - } else { - treeNode.expandParents(); - } - - this.boards.forEach((board) => { - board.scrollToNode(treeNode, detail); - }); - }); - designer.dragon.onDragend(() => { - const endTime: any = Date.now() / 1000; - const editor = designer?.editor; - const nodes = designer.currentSelection?.getNodes(); - editor?.emit('outlinePane.drag', { - selected: nodes - ?.map((n) => { - if (!n) { - return; - } - const npm = n?.componentMeta?.npm; - return ( - [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || n?.componentMeta?.componentName - ); - }) - .join('&'), - time: (endTime - startTime).toFixed(2), - }); - }); - designer.editor.on('designer.document.remove', ({ id }) => { - this.treeMap.delete(id); - }); - } - - private toVision() { - const tree = this.currentTree; - if (tree) { - tree.document.selection.getTopNodes().forEach((node) => { - tree.getTreeNode(node).setExpanded(false); - }); - } - } - - @obx.shallow private boards = new Set<ITreeBoard>(); - - addBoard(board: ITreeBoard) { - this.boards.add(board); - } - - removeBoard(board: ITreeBoard) { - this.boards.delete(board); - } - - hasVisibleTreeBoard() { - for (const item of this.boards) { - if (item.visible && item.at !== Backup) { - return true; - } - } - return false; - } - - purge() { - // todo others purge - } - - private treeMap = new Map<string, Tree>(); - - @computed get currentTree(): Tree | null { - const doc = this.designer?.currentDocument; - if (doc) { - const { id } = doc; - if (this.treeMap.has(id)) { - return this.treeMap.get(id)!; - } - const tree = new Tree(doc); - this.treeMap.set(id, tree); - return tree; - } - return null; - } -} - -const mastersMap = new Map<Designer, TreeMaster>(); -export function getTreeMaster(designer: Designer): TreeMaster { - let master = mastersMap.get(designer); - if (!master) { - master = new TreeMaster(designer); - mastersMap.set(designer, master); - } - return master; -} diff --git a/packages/plugin-outline-pane/src/tree-node.ts b/packages/plugin-outline-pane/src/tree-node.ts deleted file mode 100644 index 67efe5fbac..0000000000 --- a/packages/plugin-outline-pane/src/tree-node.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { TitleContent, isI18nData } from '@alilc/lowcode-types'; -import { computed, obx, intl, makeObservable, action } from '@alilc/lowcode-editor-core'; -import { Node, DocumentModel, isLocationChildrenDetail, LocationChildrenDetail, Designer } from '@alilc/lowcode-designer'; -import { Tree } from './tree'; - -/** - * 大纲树过滤结果 - */ -export interface FilterResult { - // 过滤条件是否生效 - filterWorking: boolean; - // 命中子节点 - matchChild: boolean; - // 命中本节点 - matchSelf: boolean; - // 关键字 - keywords: string; -} - -export default class TreeNode { - get id(): string { - return this.node.id; - } - - /** - * 是否可以展开 - */ - get expandable(): boolean { - if (this.locked) return false; - return this.hasChildren() || this.hasSlots() || this.dropDetail?.index != null; - } - - /** - * 插入"线"位置信息 - */ - @computed get dropDetail(): LocationChildrenDetail | undefined | null { - const loc = this.node.document.dropLocation; - return loc && this.isResponseDropping() && isLocationChildrenDetail(loc.detail) ? loc.detail : null; - } - - @computed get depth(): number { - return this.node.zLevel; - } - - isRoot(includeOriginalRoot = false) { - return this.tree.root === this || (includeOriginalRoot && this.tree.document.rootNode === this.node); - } - - /** - * 是否是响应投放区 - */ - isResponseDropping(): boolean { - const loc = this.node.document.dropLocation; - if (!loc) { - return false; - } - return loc.target === this.node; - } - - isFocusingNode(): boolean { - const loc = this.node.document.dropLocation; - if (!loc) { - return false; - } - return ( - isLocationChildrenDetail(loc.detail) && loc.detail.focus?.type === 'node' && loc.detail.focus.node === this.node - ); - } - - /** - * 默认为折叠状态 - * 在初始化根节点时,设置为展开状态 - */ - @obx.ref private _expanded = false; - - get expanded(): boolean { - return this.isRoot(true) || (this.expandable && this._expanded); - } - - setExpanded(value: boolean) { - this._expanded = value; - } - - @computed get detecting() { - return this.designer.detecting.current === this.node; - } - - @computed get hidden(): boolean { - const cv = this.node.isConditionalVisible(); - if (cv == null) { - return !this.node.getVisible(); - } - return !cv; - } - - setHidden(flag: boolean) { - if (this.node.conditionGroup) { - return; - } - this.node.setVisible(!flag); - } - - @computed get locked(): boolean { - return this.node.isLocked; - } - - setLocked(flag: boolean) { - this.node.lock(flag); - } - - @computed get selected(): boolean { - // TODO: check is dragging - const { selection } = this.document; - return selection.has(this.node.id); - } - - @computed get title(): TitleContent { - return this.node.title; - } - - @computed get titleLabel() { - let { title } = this; - if (!title) { - return ''; - } - if ((title as any).label) { - title = (title as any).label; - } - if (typeof title === 'string') { - return title; - } - if (isI18nData(title)) { - return intl(title) as string; - } - return this.node.componentName; - } - - setTitleLabel(label: string) { - const origLabel = this.titleLabel; - if (label === origLabel) { - return; - } - if (label === '') { - this.node.getExtraProp('title', false)?.remove(); - } else { - this.node.getExtraProp('title', true)?.setValue(label); - } - } - - get icon() { - return this.node.componentMeta.icon; - } - - @computed get parent(): TreeNode | null { - const { parent } = this.node; - if (parent) { - return this.tree.getTreeNode(parent); - } - return null; - } - - @computed get slots(): TreeNode[] { - // todo: shallowEqual - return this.node.slots.map((node) => this.tree.getTreeNode(node)); - } - - @computed get children(): TreeNode[] | null { - return this.node.children?.map((node) => this.tree.getTreeNode(node)) || null; - } - - /** - * 是否是容器,允许子节点拖入 - */ - isContainer(): boolean { - return this.node.isContainer(); - } - - /** - * 判断是否有"插槽" - */ - hasSlots(): boolean { - return this.node.hasSlots(); - } - - hasChildren(): boolean { - return !!(this.isContainer() && this.node.children?.notEmpty()); - } - - select(isMulti: boolean) { - const { node } = this; - - const { selection } = node.document; - if (isMulti) { - selection.add(node.id); - } else { - selection.select(node.id); - } - } - - /** - * 展开节点,支持依次展开父节点 - */ - expand(tryExpandParents = false) { - // 这边不能直接使用 expanded,需要额外判断是否可以展开 - // 如果只使用 expanded,会漏掉不可以展开的情况,即在不可以展开的情况下,会触发展开 - if (this.expandable && !this._expanded) { - this.setExpanded(true); - } - if (tryExpandParents) { - this.expandParents(); - } - } - - expandParents() { - let p = this.node.parent; - while (p) { - this.tree.getTreeNode(p).setExpanded(true); - p = p.parent; - } - } - - readonly designer: Designer; - - readonly document: DocumentModel; - - @obx.ref private _node: Node; - - get node() { - return this._node; - } - - readonly tree: Tree; - - constructor(tree: Tree, node: Node) { - makeObservable(this); - this.tree = tree; - this.document = node.document; - this.designer = this.document.designer; - this._node = node; - } - - @action - setNode(node: Node) { - if (this._node !== node) { - this._node = node; - } - } - - @obx.ref private _filterResult: FilterResult = { - filterWorking: false, - matchChild: false, - matchSelf: false, - keywords: '', - }; - - get filterReult(): FilterResult { - return this._filterResult; - } - - @action - setFilterReult(val: FilterResult) { - this._filterResult = val; - } -} diff --git a/packages/plugin-outline-pane/src/tree.ts b/packages/plugin-outline-pane/src/tree.ts deleted file mode 100644 index c98fe90dd2..0000000000 --- a/packages/plugin-outline-pane/src/tree.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { DocumentModel, Node } from '@alilc/lowcode-designer'; -import { computed, makeObservable } from '@alilc/lowcode-editor-core'; -import TreeNode from './tree-node'; - -export class Tree { - private treeNodesMap = new Map<string, TreeNode>(); - - readonly id: string; - - @computed get root(): TreeNode | null { - if (this.document.focusNode) { - return this.getTreeNode(this.document.focusNode!); - } - return null; - } - - constructor(readonly document: DocumentModel) { - makeObservable(this); - this.id = document.id; - } - - getTreeNode(node: Node): TreeNode { - if (this.treeNodesMap.has(node.id)) { - const tnode = this.treeNodesMap.get(node.id)!; - tnode.setNode(node); - return tnode; - } - - const treeNode = new TreeNode(this, node); - this.treeNodesMap.set(node.id, treeNode); - return treeNode; - } - - getTreeNodeById(id: string) { - return this.treeNodesMap.get(id); - } -} diff --git a/packages/plugin-outline-pane/src/views/backup-pane.tsx b/packages/plugin-outline-pane/src/views/backup-pane.tsx deleted file mode 100644 index f4980a9bce..0000000000 --- a/packages/plugin-outline-pane/src/views/backup-pane.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { PureComponent } from 'react'; -import { globalContext } from '@alilc/lowcode-editor-core'; -import { PluginProps } from '@alilc/lowcode-types'; -import { OutlinePane } from './pane'; - -export const Backup = Symbol.for('backup-outline'); - -export class OutlineBackupPane extends PureComponent<PluginProps> { - render() { - return ( - <OutlinePane - editor={globalContext.get('editor')} - config={{ - name: Backup, - }} - /> - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/filter-tree.ts b/packages/plugin-outline-pane/src/views/filter-tree.ts index 2f38ada57a..793aa03cc5 100644 --- a/packages/plugin-outline-pane/src/views/filter-tree.ts +++ b/packages/plugin-outline-pane/src/views/filter-tree.ts @@ -1,4 +1,4 @@ -import TreeNode from '../tree-node'; +import TreeNode from '../controllers/tree-node'; export const FilterType = { CONDITION: 'CONDITION', @@ -9,16 +9,16 @@ export const FilterType = { export const FILTER_OPTIONS = [{ value: FilterType.CONDITION, - label: '条件渲染', + label: 'Conditional rendering', }, { value: FilterType.LOOP, - label: '循环渲染', + label: 'Loop rendering', }, { value: FilterType.LOCKED, - label: '已锁定', + label: 'Locked', }, { value: FilterType.HIDDEN, - label: '已隐藏', + label: 'Hidden', }]; export const matchTreeNode = ( @@ -77,6 +77,11 @@ export const matchTreeNode = ( return matchTreeNode(childNode, keywords, filterOps); }).find(Boolean); + // 如果命中了子节点,需要将该节点展开 + if (matchChild && treeNode.expandable) { + treeNode.setExpanded(true); + } + treeNode.setFilterReult({ filterWorking: true, matchChild, diff --git a/packages/plugin-outline-pane/src/views/filter.tsx b/packages/plugin-outline-pane/src/views/filter.tsx index 67c84fa3da..8fd18ed251 100644 --- a/packages/plugin-outline-pane/src/views/filter.tsx +++ b/packages/plugin-outline-pane/src/views/filter.tsx @@ -1,21 +1,17 @@ -import React, { Component } from 'react'; +import React, { PureComponent } from 'react'; import './style.less'; import { IconFilter } from '../icons/filter'; import { Search, Checkbox, Balloon, Divider } from '@alifd/next'; -import TreeNode from '../tree-node'; -import { Tree } from '../tree'; +import TreeNode from '../controllers/tree-node'; +import { Tree } from '../controllers/tree'; import { matchTreeNode, FILTER_OPTIONS } from './filter-tree'; -interface IState { +export default class Filter extends PureComponent<{ + tree: Tree; +}, { keywords: string; filterOps: string[]; -} - -interface IProps { - tree: Tree; -} - -export default class Filter extends Component<IProps, IState> { +}> { state = { keywords: '', filterOps: [], @@ -58,7 +54,7 @@ export default class Filter extends Component<IProps, IState> { <Search hasClear shape="simple" - placeholder="过滤节点" + placeholder={this.props.tree.pluginContext.intl('Filter Node')} className="lc-outline-filter-search-input" value={keywords} onChange={this.handleSearchChange} @@ -79,7 +75,7 @@ export default class Filter extends Component<IProps, IState> { indeterminate={indeterminate} onChange={this.handleCheckAll} > - 全选 + {this.props.tree.pluginContext.intlNode('Check All')} </Checkbox> <Divider /> <Checkbox.Group @@ -93,7 +89,7 @@ export default class Filter extends Component<IProps, IState> { value={op.value} key={op.value} > - {op.label} + {this.props.tree.pluginContext.intlNode(op.label)} </Checkbox> ))} </Checkbox.Group> diff --git a/packages/plugin-outline-pane/src/views/pane.tsx b/packages/plugin-outline-pane/src/views/pane.tsx index a57bfad92f..4b807ca180 100644 --- a/packages/plugin-outline-pane/src/views/pane.tsx +++ b/packages/plugin-outline-pane/src/views/pane.tsx @@ -1,35 +1,74 @@ -import React, { Component } from 'react'; -import { observer, globalContext } from '@alilc/lowcode-editor-core'; -import { intl } from '../locale'; -import { OutlineMain } from '../main'; +import React, { PureComponent } from 'react'; +import { Loading } from '@alifd/next'; +import { PaneController } from '../controllers/pane-controller'; import TreeView from './tree'; import './style.less'; -import { IEditor } from '@alilc/lowcode-types'; import Filter from './filter'; +import { TreeMaster } from '../controllers/tree-master'; +import { Tree } from '../controllers/tree'; +import { IPublicTypeDisposable } from '@alilc/lowcode-types'; -@observer -export class OutlinePane extends Component<{ config: any; editor: IEditor }> { - private main = new OutlineMain(globalContext.get('editor'), this.props.config.name || this.props.config.pluginKey); +export class Pane extends PureComponent<{ + treeMaster: TreeMaster; + controller: PaneController; + hideFilter?: boolean; +}, { + tree: Tree | null; +}> { + private controller; + + private simulatorRendererReadyDispose: IPublicTypeDisposable; + private changeDocumentDispose: IPublicTypeDisposable; + private removeDocumentDispose: IPublicTypeDisposable; + + constructor(props: any) { + super(props); + const { controller, treeMaster } = props; + this.controller = controller; + this.state = { + tree: treeMaster.currentTree, + }; + this.simulatorRendererReadyDispose = this.props.treeMaster.pluginContext?.project?.onSimulatorRendererReady(this.changeTree); + this.changeDocumentDispose = this.props.treeMaster.pluginContext?.project?.onChangeDocument(this.changeTree); + this.removeDocumentDispose = this.props.treeMaster.pluginContext?.project?.onRemoveDocument(this.changeTree); + } + + changeTree = () => { + this.setState({ + tree: this.props.treeMaster.currentTree, + }); + }; componentWillUnmount() { - this.main.purge(); + this.controller.purge(); + this.simulatorRendererReadyDispose?.(); + this.changeDocumentDispose?.(); + this.removeDocumentDispose?.(); } render() { - const tree = this.main.currentTree; + const tree = this.state.tree; if (!tree) { return ( <div className="lc-outline-pane"> - <p className="lc-outline-notice">{intl('Initializing')}</p> + <p className="lc-outline-notice"> + <Loading + style={{ + display: 'block', + marginTop: '40px', + }} + tip={this.props.treeMaster.pluginContext.intl('Initializing')} + /> + </p> </div> ); } return ( <div className="lc-outline-pane"> - <Filter tree={tree} /> - <div ref={(shell) => this.main.mount(shell)} className="lc-outline-tree-container"> + { !this.props.hideFilter && <Filter tree={tree} /> } + <div ref={(shell) => this.controller.mount(shell)} className={`lc-outline-tree-container ${ this.props.hideFilter ? 'lc-hidden-outline-filter' : '' }`}> <TreeView key={tree.id} tree={tree} /> </div> </div> diff --git a/packages/plugin-outline-pane/src/views/root-tree-node.tsx b/packages/plugin-outline-pane/src/views/root-tree-node.tsx deleted file mode 100644 index f0819aeae2..0000000000 --- a/packages/plugin-outline-pane/src/views/root-tree-node.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Component } from 'react'; -import classNames from 'classnames'; -import { observer } from '@alilc/lowcode-editor-core'; -import { ModalNodesManager } from '@alilc/lowcode-designer'; -import TreeNode from '../tree-node'; -import TreeTitle from './tree-title'; -import TreeBranches from './tree-branches'; -import { IconEyeClose } from '../icons/eye-close'; - -@observer -class ModalTreeNodeView extends Component<{ treeNode: TreeNode }> { - private modalNodesManager: ModalNodesManager; - - constructor(props: any) { - super(props); - - // 模态管理对象 - this.modalNodesManager = props.treeNode.document.modalNodesManager; - } - - hideAllNodes() { - this.modalNodesManager.hideModalNodes(); - } - - render() { - const { treeNode } = this.props; - // 当指定了新的根节点时,要从原始的根节点去获取模态节点 - const rootTreeNode = treeNode.tree.getTreeNode(treeNode.document.rootNode!); - const modalNodes = rootTreeNode.children?.filter((item) => { - return item.node.componentMeta.isModal; - }); - if (!modalNodes || modalNodes.length === 0) { - return null; - } - - const hasVisibleModalNode = !!this.modalNodesManager.getVisibleModalNode(); - return ( - <div className="tree-node-modal"> - <div className="tree-node-modal-title"> - <span>模态视图层</span> - <div - className="tree-node-modal-title-visible-icon" - onClick={this.hideAllNodes.bind(this)} - > - {hasVisibleModalNode ? <IconEyeClose /> : null} - </div> - </div> - <div className="tree-pane-modal-content"> - <TreeBranches treeNode={rootTreeNode} isModal /> - </div> - </div> - ); - } -} - -@observer -export default class RootTreeNodeView extends Component<{ treeNode: TreeNode }> { - render() { - const { treeNode } = this.props; - const className = classNames('tree-node', { - // 是否展开 - expanded: treeNode.expanded, - // 是否悬停中 - detecting: treeNode.detecting, - // 是否选中的 - selected: treeNode.selected, - // 是否隐藏的 - hidden: treeNode.hidden, - // 是否忽略的 - // ignored: treeNode.ignored, - // 是否锁定的 - locked: treeNode.locked, - // 是否投放响应 - dropping: treeNode.dropDetail?.index != null, - 'is-root': treeNode.isRoot(), - 'condition-flow': treeNode.node.conditionGroup != null, - highlight: treeNode.isFocusingNode(), - }); - - return ( - <div className={className} data-id={treeNode.id}> - <TreeTitle treeNode={treeNode} /> - <ModalTreeNodeView treeNode={treeNode} /> - <TreeBranches treeNode={treeNode} /> - </div> - ); - } -} diff --git a/packages/plugin-outline-pane/src/views/style.less b/packages/plugin-outline-pane/src/views/style.less index 423dbbb4ea..8a38ba749b 100644 --- a/packages/plugin-outline-pane/src/views/style.less +++ b/packages/plugin-outline-pane/src/views/style.less @@ -3,7 +3,6 @@ width: 100%; position: relative; z-index: 200; - background-color: white; > .lc-outline-tree-container { top: 52px; @@ -14,10 +13,14 @@ overflow: auto; } + > .lc-outline-tree-container.lc-hidden-outline-filter { + top: 0; + } + > .lc-outline-filter { padding: 12px 16px; display: flex; - align-items: center; + align-items: stretch; justify-content: right; .lc-outline-filter-search-input { @@ -25,9 +28,8 @@ } .lc-outline-filter-icon { - background: #ebecf0; - border: 1px solid #c4c6cf; - height: 28px; + background: var(--color-block-background-light, #ebecf0); + border: 1px solid var(--color-field-border, #c4c6cf); display: flex; align-items: center; border-radius: 0 2px 2px 0; @@ -48,20 +50,21 @@ overflow: hidden; margin-bottom: @treeNodeHeight; user-select: none; + overflow-x: scroll; .tree-node-modal { margin: 5px; - border: 1px solid rgba(31, 56, 88, 0.2); + border: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.2)); border-radius: 3px; - box-shadow: 0 1px 4px 0 rgba(31, 56, 88, 0.15); + box-shadow: 0 1px 4px 0 var(--color-block-background-shallow, rgba(31, 56, 88, 0.15)); .tree-node-modal-title { position: relative; - background: rgba(31, 56, 88, 0.04); + background: var(--color-block-background-light, rgba(31, 56, 88, 0.04)); padding: 0 10px; height: 32px; line-height: 32px; - border-bottom: 1px solid rgba(31, 56, 88, 0.2); + border-bottom: 1px solid var(--color-field-border, rgba(31, 56, 88, 0.2)); .tree-node-modal-title-visible-icon { position: absolute; @@ -77,7 +80,8 @@ } } - .tree-node-modal-radio, .tree-node-modal-radio-active { + .tree-node-modal-radio, + .tree-node-modal-radio-active { margin-right: 4px; opacity: 0.8; position: absolute; @@ -85,7 +89,7 @@ left: 6px; } .tree-node-modal-radio-active { - color: #006cff; + color: var(--color-brand, #006cff); } } @@ -104,7 +108,7 @@ &:hover { .tree-node-branches::before { - border-left-color: #ddd; + border-left-color: var(--color-line-darken, #ddd); } } @@ -116,75 +120,75 @@ transform: translateZ(0); transition: all 0.2s ease-in-out; &.invalid { - border-color: red; - background-color: rgba(240, 154, 154, 0.719); + border-color: var(--color-error, var(--color-function-error, red)); + background-color: var(--color-block-background-error, rgba(240, 154, 154, 0.719)); } } .condition-group-container { - border-bottom: 1px solid #7b605b; + border-bottom: 1px solid var(--color-brown, var(--color-function-brown, #7b605b)); position: relative; &:before { position: absolute; display: block; width: 0; - border-left: 0.5px solid #7b605b; + border-left: 0.5px solid var(--color-brown, var(--color-function-brown, #7b605b)); height: 100%; top: 0; left: 0; content: ' '; z-index: 2; } - >.condition-group-title { + > .condition-group-title { text-align: center; - background-color: #7b605b; + background-color: var(--color-brown, var(--color-function-brown, #7b605b)); height: 14px; > .lc-title { font-size: 12px; transform: scale(0.8); transform-origin: top; - color: white; - text-shadow: 0 0 2px black; + color: var(--color-text-reverse, white); + text-shadow: 0 0 2px var(--color-block-background-shallow, black); display: block; } } } .tree-node-slots { - border-bottom: 1px solid rgb(144, 94, 190); + border-bottom: 1px solid var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); position: relative; &::before { position: absolute; display: block; width: 0; - border-left: 0.5px solid rgb(144, 94, 190); + border-left: 0.5px solid var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); height: 100%; top: 0; left: 0; content: ' '; z-index: 2; } - >.tree-node-slots-title { + > .tree-node-slots-title { text-align: center; - background-color: rgb(144, 94, 190); + background-color: var(--color-purple, var(--color-function-purple, rgb(144, 94, 190))); height: 14px; > .lc-title { font-size: 12px; transform: scale(0.8); transform-origin: top; - color: white; + color: var(--color-text-reverse, white); text-shadow: 0 0 2px black; display: block; } } &.insertion-at-slots { padding-bottom: @treeNodeHeight; - border-bottom-color: rgb(182, 55, 55); - >.tree-node-slots-title { - background-color: rgb(182, 55, 55); + border-bottom-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); + > .tree-node-slots-title { + background-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); } &::before { - border-left-color: rgb(182, 55, 55); + border-left-color: var(--color-error-dark, var(--color-function-error-dark, rgb(182, 55, 55))); } } } @@ -240,7 +244,6 @@ .tree-node-title { font-size: var(--font-size-text); cursor: pointer; - background: var(--color-pane-background); border-bottom: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1)); display: flex; align-items: center; @@ -276,7 +279,10 @@ } } - .tree-node-hide-btn, .tree-node-lock-btn { + .tree-node-hide-btn, + .tree-node-lock-btn, + .tree-node-rename-btn, + .tree-node-delete-btn { opacity: 0; color: var(--color-text); line-height: 0; @@ -290,18 +296,26 @@ } } &:hover { - .tree-node-hide-btn, .tree-node-lock-btn { + .tree-node-hide-btn, + .tree-node-lock-btn, + .tree-node-rename-btn, + .tree-node-delete-btn { opacity: 0.5; } } html.lc-cursor-dragging & { // FIXME: only hide hover shows - .tree-node-hide-btn, .tree-node-lock-btn { + .tree-node-hide-btn, + .tree-node-lock-btn, + .tree-node-rename-btn { display: none; } } &.editing { - & > .tree-node-hide-btn, & >.tree-node-lock-btn { + & > .tree-node-hide-btn, + & > .tree-node-lock-btn, + & > .tree-node-rename-btn, + & > .tree-node-delete-btn { display: none; } } @@ -312,13 +326,13 @@ align-items: center; line-height: 0; &.cond { - color: rgb(179, 52, 6); + color: var(--color-error, var(--color-function-error, rgb(179, 52, 6))); } &.loop { - color: rgb(103, 187, 187); + color: var(--color-success, var(--color-function-success, rgb(103, 187, 187))); } &.slot { - color: rgb(211, 90, 211); + color: var(--color-purple, var(--color-function-purple, rgb(211, 90, 211))); } } } @@ -342,7 +356,7 @@ // 选中节点处理 &.selected { & > .tree-node-title { - background: var(--color-block-background-shallow); + background: var(--color-block-background-light); } & > .tree-node-branches::before { @@ -352,7 +366,7 @@ &.hidden { .tree-node-title-label { - color: #9b9b9b; + color: var(--color-text-disabled, #9b9b9b); } & > .tree-node-title > .tree-node-hide-btn { opacity: 0.8; @@ -378,7 +392,8 @@ opacity: 0.8; } .tree-node-branches { - .tree-node-lock-btn, .tree-node-hide-btn { + .tree-node-lock-btn, + .tree-node-hide-btn { opacity: 0; } } diff --git a/packages/plugin-outline-pane/src/views/tree-branches.tsx b/packages/plugin-outline-pane/src/views/tree-branches.tsx index 4521fe2e72..41bd694812 100644 --- a/packages/plugin-outline-pane/src/views/tree-branches.tsx +++ b/packages/plugin-outline-pane/src/views/tree-branches.tsx @@ -1,20 +1,45 @@ -import { Component } from 'react'; +import { PureComponent } from 'react'; import classNames from 'classnames'; -import { observer, Title } from '@alilc/lowcode-editor-core'; -import { ExclusiveGroup } from '@alilc/lowcode-designer'; -import TreeNode from '../tree-node'; +import TreeNode from '../controllers/tree-node'; import TreeNodeView from './tree-node'; -import { intlNode } from '../locale'; +import { IPublicModelExclusiveGroup, IPublicTypeDisposable, IPublicTypeLocationChildrenDetail } from '@alilc/lowcode-types'; -@observer -export default class TreeBranches extends Component<{ +export default class TreeBranches extends PureComponent<{ treeNode: TreeNode; isModal?: boolean; + expanded: boolean; + treeChildren: TreeNode[] | null; }> { - render() { - const { treeNode, isModal } = this.props; - const { expanded } = treeNode; + state = { + filterWorking: false, + matchChild: false, + }; + private offExpandedChanged: (() => void) | null; + constructor(props: any) { + super(props); + + const { treeNode } = this.props; const { filterWorking, matchChild } = treeNode.filterReult; + this.setState({ filterWorking, matchChild }); + } + + componentDidMount() { + const { treeNode } = this.props; + treeNode.onFilterResultChanged(() => { + const { filterWorking: newFilterWorking, matchChild: newMatchChild } = treeNode.filterReult; + this.setState({ filterWorking: newFilterWorking, matchChild: newMatchChild }); + }); + } + + componentWillUnmount(): void { + if (this.offExpandedChanged) { + this.offExpandedChanged(); + } + } + + render() { + const { treeNode, isModal, expanded } = this.props; + const { filterWorking, matchChild } = this.state; // 条件过滤生效时,如果命中了子节点,需要将该节点展开 const expandInFilterResult = filterWorking && matchChild; @@ -27,29 +52,81 @@ export default class TreeBranches extends Component<{ { !isModal && <TreeNodeSlots treeNode={treeNode} /> } - <TreeNodeChildren treeNode={treeNode} isModal={isModal || false} /> + <TreeNodeChildren + treeNode={treeNode} + isModal={isModal || false} + treeChildren={this.props.treeChildren} + /> </div> ); } } -@observer -class TreeNodeChildren extends Component<{ +interface ITreeNodeChildrenState { + filterWorking: boolean; + matchSelf: boolean; + keywords: string | null; + dropDetail: IPublicTypeLocationChildrenDetail | undefined | null; +} +class TreeNodeChildren extends PureComponent<{ treeNode: TreeNode; isModal?: boolean; - }> { + treeChildren: TreeNode[] | null; + }, ITreeNodeChildrenState> { + state: ITreeNodeChildrenState = { + filterWorking: false, + matchSelf: false, + keywords: null, + dropDetail: null, + }; + offLocationChanged: IPublicTypeDisposable | undefined; + componentDidMount() { + const { treeNode } = this.props; + const { project } = treeNode.pluginContext; + const { filterWorking, matchSelf, keywords } = treeNode.filterReult; + const { dropDetail } = treeNode; + this.setState({ + filterWorking, + matchSelf, + keywords, + dropDetail, + }); + treeNode.onFilterResultChanged(() => { + const { + filterWorking: newFilterWorking, + matchSelf: newMatchChild, + keywords: newKeywords, + } = treeNode.filterReult; + this.setState({ + filterWorking: newFilterWorking, + matchSelf: newMatchChild, + keywords: newKeywords, + }); + }); + this.offLocationChanged = project.currentDocument?.onDropLocationChanged( + () => { + this.setState({ dropDetail: treeNode.dropDetail }); + }, + ); + } + componentWillUnmount(): void { + this.offLocationChanged && this.offLocationChanged(); + } + render() { - const { treeNode, isModal } = this.props; + const { isModal } = this.props; const children: any = []; let groupContents: any[] = []; - let currentGrp: ExclusiveGroup; - const { filterWorking, matchSelf, keywords } = treeNode.filterReult; + let currentGrp: IPublicModelExclusiveGroup; + const { filterWorking, matchSelf, keywords } = this.state; + const Title = this.props.treeNode.pluginContext.common.editorCabin.Title; const endGroup = () => { if (groupContents.length > 0) { children.push( - <div key={currentGrp.id} className="condition-group-container" data-id={currentGrp.firstNode.id}> + <div key={currentGrp.id} className="condition-group-container" data-id={currentGrp.firstNode?.id}> <div className="condition-group-title"> + {/* @ts-ignore */} <Title title={currentGrp.title} match={filterWorking && matchSelf} @@ -62,7 +139,8 @@ class TreeNodeChildren extends Component<{ groupContents = []; } }; - const { dropDetail } = treeNode; + + const { dropDetail } = this.state; const dropIndex = dropDetail?.index; const insertion = ( <div @@ -72,8 +150,8 @@ class TreeNodeChildren extends Component<{ })} /> ); - treeNode.children?.forEach((child, index) => { - const childIsModal = child.node.componentMeta.isModal || false; + this.props.treeChildren?.forEach((child, index) => { + const childIsModal = child.node.componentMeta?.isModal || false; if (isModal != childIsModal) { return; } @@ -91,16 +169,16 @@ class TreeNodeChildren extends Component<{ children.push(insertion); } } - groupContents.push(<TreeNodeView key={child.id} treeNode={child} isModal={isModal} />); + groupContents.push(<TreeNodeView key={child.nodeId} treeNode={child} isModal={isModal} />); } else { if (index === dropIndex) { children.push(insertion); } - children.push(<TreeNodeView key={child.id} treeNode={child} isModal={isModal} />); + children.push(<TreeNodeView key={child.nodeId} treeNode={child} isModal={isModal} />); } }); endGroup(); - const length = treeNode.children?.length || 0; + const length = this.props.treeChildren?.length || 0; if (dropIndex != null && dropIndex >= length) { children.push(insertion); } @@ -109,8 +187,7 @@ class TreeNodeChildren extends Component<{ } } -@observer -class TreeNodeSlots extends Component<{ +class TreeNodeSlots extends PureComponent<{ treeNode: TreeNode; }> { render() { @@ -118,18 +195,20 @@ class TreeNodeSlots extends Component<{ if (!treeNode.hasSlots()) { return null; } + const Title = this.props.treeNode.pluginContext.common.editorCabin.Title; return ( <div className={classNames('tree-node-slots', { 'insertion-at-slots': treeNode.dropDetail?.focus?.type === 'slots', })} - data-id={treeNode.id} + data-id={treeNode.nodeId} > <div className="tree-node-slots-title"> - <Title title={{ type: 'i18n', intl: intlNode('Slots') }} /> + {/* @ts-ignore */} + <Title title={{ type: 'i18n', intl: this.props.treeNode.pluginContext.intlNode('Slots') }} /> </div> {treeNode.slots.map(tnode => ( - <TreeNodeView key={tnode.id} treeNode={tnode} /> + <TreeNodeView key={tnode.nodeId} treeNode={tnode} /> ))} </div> ); diff --git a/packages/plugin-outline-pane/src/views/tree-node.tsx b/packages/plugin-outline-pane/src/views/tree-node.tsx index 282be0a216..11bd95d12f 100644 --- a/packages/plugin-outline-pane/src/views/tree-node.tsx +++ b/packages/plugin-outline-pane/src/views/tree-node.tsx @@ -1,48 +1,261 @@ -import { Component } from 'react'; +import { PureComponent } from 'react'; import classNames from 'classnames'; -import { observer } from '@alilc/lowcode-editor-core'; -import TreeNode from '../tree-node'; +import TreeNode from '../controllers/tree-node'; import TreeTitle from './tree-title'; import TreeBranches from './tree-branches'; +import { IconEyeClose } from '../icons/eye-close'; +import { IPublicModelModalNodesManager, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { IOutlinePanelPluginContext } from '../controllers/tree-master'; -@observer -export default class TreeNodeView extends Component<{ +class ModalTreeNodeView extends PureComponent<{ treeNode: TreeNode; - isModal?: boolean; +}, { + treeChildren: TreeNode[] | null; }> { + private modalNodesManager: IPublicModelModalNodesManager | undefined | null; + readonly pluginContext: IOutlinePanelPluginContext; + + constructor(props: { + treeNode: TreeNode; + }) { + super(props); + + // 模态管理对象 + this.pluginContext = props.treeNode.pluginContext; + const { project } = this.pluginContext; + this.modalNodesManager = project.currentDocument?.modalNodesManager; + this.state = { + treeChildren: this.rootTreeNode.children, + }; + } + + hideAllNodes() { + this.modalNodesManager?.hideModalNodes(); + } + + componentDidMount(): void { + const { rootTreeNode } = this; + rootTreeNode.onExpandableChanged(() => { + this.setState({ + treeChildren: rootTreeNode.children, + }); + }); + } + + get rootTreeNode() { + const { treeNode } = this.props; + // 当指定了新的根节点时,要从原始的根节点去获取模态节点 + const { project } = this.pluginContext; + const rootNode = project.currentDocument?.root; + const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); + + return rootTreeNode; + } + render() { - const { treeNode, isModal } = this.props; - const className = classNames('tree-node', { - // 是否展开 - expanded: treeNode.expanded, - // 是否悬停中 - detecting: treeNode.detecting, - // 是否选中的 + const { rootTreeNode } = this; + const { expanded } = rootTreeNode; + + const hasVisibleModalNode = !!this.modalNodesManager?.getVisibleModalNode(); + return ( + <div className="tree-node-modal"> + <div className="tree-node-modal-title"> + <span>{this.pluginContext.intlNode('Modal View')}</span> + <div + className="tree-node-modal-title-visible-icon" + onClick={this.hideAllNodes.bind(this)} + > + {hasVisibleModalNode ? <IconEyeClose /> : null} + </div> + </div> + <div className="tree-pane-modal-content"> + <TreeBranches + treeNode={rootTreeNode} + treeChildren={this.state.treeChildren} + expanded={expanded} + isModal + /> + </div> + </div> + ); + } +} + +export default class TreeNodeView extends PureComponent<{ + treeNode: TreeNode; + isModal?: boolean; + isRootNode?: boolean; +}> { + state: { + expanded: boolean; + selected: boolean; + hidden: boolean; + locked: boolean; + detecting: boolean; + isRoot: boolean; + highlight: boolean; + dropping: boolean; + conditionFlow: boolean; + expandable: boolean; + treeChildren: TreeNode[] | null; + filterWorking: boolean; + matchChild: boolean; + matchSelf: boolean; + } = { + expanded: false, + selected: false, + hidden: false, + locked: false, + detecting: false, + isRoot: false, + highlight: false, + dropping: false, + conditionFlow: false, + expandable: false, + treeChildren: [], + filterWorking: false, + matchChild: false, + matchSelf: false, + }; + + eventOffCallbacks: Array<IPublicTypeDisposable | undefined> = []; + constructor(props: any) { + super(props); + + const { treeNode, isRootNode } = this.props; + this.state = { + expanded: isRootNode ? true : treeNode.expanded, selected: treeNode.selected, - // 是否隐藏的 hidden: treeNode.hidden, - // 是否忽略的 - // ignored: treeNode.ignored, - // 是否锁定的 locked: treeNode.locked, + detecting: treeNode.detecting, + isRoot: treeNode.isRoot(), // 是否投放响应 dropping: treeNode.dropDetail?.index != null, - 'is-root': treeNode.isRoot(), - 'condition-flow': treeNode.node.conditionGroup != null, + conditionFlow: treeNode.node.conditionGroup != null, highlight: treeNode.isFocusingNode(), + expandable: treeNode.expandable, + treeChildren: treeNode.children, + }; + } + + componentDidMount() { + const { treeNode } = this.props; + const { project } = treeNode.pluginContext; + + const doc = project.currentDocument; + + treeNode.onExpandedChanged(((expanded: boolean) => { + this.setState({ expanded }); + })); + treeNode.onHiddenChanged((hidden: boolean) => { + this.setState({ hidden }); + }); + treeNode.onLockedChanged((locked: boolean) => { + this.setState({ locked }); + }); + treeNode.onExpandableChanged((expandable: boolean) => { + this.setState({ + expandable, + treeChildren: treeNode.children, + }); }); + treeNode.onFilterResultChanged(() => { + const { filterWorking: newFilterWorking, matchChild: newMatchChild, matchSelf: newMatchSelf } = treeNode.filterReult; + this.setState({ filterWorking: newFilterWorking, matchChild: newMatchChild, matchSelf: newMatchSelf }); + }); + this.eventOffCallbacks.push( + doc?.onDropLocationChanged(() => { + this.setState({ + dropping: treeNode.dropDetail?.index != null, + }); + }), + ); - const { filterWorking, matchChild, matchSelf } = treeNode.filterReult; + const offSelectionChange = doc?.selection?.onSelectionChange(() => { + this.setState({ selected: treeNode.selected }); + }); + this.eventOffCallbacks.push(offSelectionChange!); + const offDetectingChange = doc?.detecting?.onDetectingChange(() => { + this.setState({ detecting: treeNode.detecting }); + }); + this.eventOffCallbacks.push(offDetectingChange!); + } + componentWillUnmount(): void { + this.eventOffCallbacks?.forEach((offFun: IPublicTypeDisposable | undefined) => { + offFun && offFun(); + }); + } - // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 - if (filterWorking && !matchChild && !matchSelf) { - return null; + shouldShowModalTreeNode(): boolean { + const { treeNode, isRootNode } = this.props; + if (!isRootNode) { + // 只在 当前树 的根节点展示模态节点 + return false; } + // 当指定了新的根节点时,要从原始的根节点去获取模态节点 + const { project } = treeNode.pluginContext; + const rootNode = project.currentDocument?.root; + const rootTreeNode = treeNode.tree.getTreeNode(rootNode!); + const modalNodes = rootTreeNode.children?.filter((item) => { + return item.node.componentMeta?.isModal; + }); + return !!(modalNodes && modalNodes.length > 0); + } + + render() { + const { treeNode, isModal, isRootNode } = this.props; + const className = classNames('tree-node', { + // 是否展开 + expanded: this.state.expanded, + // 是否选中的 + selected: this.state.selected, + // 是否隐藏的 + hidden: this.state.hidden, + // 是否锁定的 + locked: this.state.locked, + // 是否悬停中 + detecting: this.state.detecting, + // 是否投放响应 + dropping: this.state.dropping, + 'is-root': this.state.isRoot, + 'condition-flow': this.state.conditionFlow, + highlight: this.state.highlight, + }); + let shouldShowModalTreeNode: boolean = this.shouldShowModalTreeNode(); + + // filter 处理 + const { filterWorking, matchChild, matchSelf } = this.state; + if (!isRootNode && filterWorking && !matchChild && !matchSelf) { + // 条件过滤生效时,如果未命中本节点或子节点,则不展示该节点 + // 根节点始终展示 + return null; + } return ( - <div className={className} data-id={treeNode.id}> - <TreeTitle treeNode={treeNode} isModal={isModal} /> - <TreeBranches treeNode={treeNode} isModal={false} /> + <div + className={className} + data-id={treeNode.nodeId} + > + <TreeTitle + treeNode={treeNode} + isModal={isModal} + expanded={this.state.expanded} + hidden={this.state.hidden} + locked={this.state.locked} + expandable={this.state.expandable} + /> + {shouldShowModalTreeNode && + <ModalTreeNodeView + treeNode={treeNode} + /> + } + <TreeBranches + treeNode={treeNode} + isModal={false} + expanded={this.state.expanded} + treeChildren={this.state.treeChildren} + /> </div> ); } diff --git a/packages/plugin-outline-pane/src/views/tree-title.tsx b/packages/plugin-outline-pane/src/views/tree-title.tsx index 3398ffc631..f822bd644b 100644 --- a/packages/plugin-outline-pane/src/views/tree-title.tsx +++ b/packages/plugin-outline-pane/src/views/tree-title.tsx @@ -1,44 +1,49 @@ -import { Component, KeyboardEvent, FocusEvent, Fragment } from 'react'; +import { KeyboardEvent, FocusEvent, Fragment, PureComponent } from 'react'; import classNames from 'classnames'; -import { observer, Title, Tip, globalContext, Editor, engineConfig } from '@alilc/lowcode-editor-core'; import { createIcon } from '@alilc/lowcode-utils'; +import { IPublicApiEvent } from '@alilc/lowcode-types'; +import TreeNode from '../controllers/tree-node'; +import { IconLock, IconUnlock, IconArrowRight, IconEyeClose, IconEye, IconCond, IconLoop, IconRadioActive, IconRadio, IconSetting, IconDelete } from '../icons'; -import { IconArrowRight } from '../icons/arrow-right'; -import { IconEyeClose } from '../icons/eye-close'; -import { intl, intlNode } from '../locale'; -import TreeNode from '../tree-node'; -import { IconEye } from '../icons/eye'; -import { IconCond } from '../icons/cond'; -import { IconLoop } from '../icons/loop'; -import { IconRadioActive } from '../icons/radio-active'; -import { IconRadio } from '../icons/radio'; -import { IconLock, IconUnlock } from '../icons'; - - -function emitOutlineEvent(type: string, treeNode: TreeNode, rest?: Record<string, unknown>) { - const editor = globalContext.get(Editor); +function emitOutlineEvent(event: IPublicApiEvent, type: string, treeNode: TreeNode, rest?: Record<string, unknown>) { const node = treeNode?.node; const npm = node?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || node?.componentMeta?.componentName || ''; - editor?.emit(`outlinePane.${type}`, { + event.emit(`outlinePane.${type}`, { selected, ...rest, }); } -@observer -export default class TreeTitle extends Component<{ +export default class TreeTitle extends PureComponent<{ treeNode: TreeNode; isModal?: boolean; + expanded: boolean; + hidden: boolean; + locked: boolean; + expandable: boolean; }> { state: { editing: boolean; + title: string; + condition?: boolean; + visible?: boolean; + filterWorking: boolean; + keywords: string; + matchSelf: boolean; } = { editing: false, + title: '', + filterWorking: false, + keywords: '', + matchSelf: false, }; - private enableEdit = () => { + private lastInput?: HTMLInputElement; + + private enableEdit = (e: MouseEvent) => { + e.preventDefault(); this.setState({ editing: true, }); @@ -55,7 +60,7 @@ export default class TreeTitle extends Component<{ const { treeNode } = this.props; const value = (e.target as HTMLInputElement).value || ''; treeNode.setTitleLabel(value); - emitOutlineEvent('rename', treeNode, { value }); + emitOutlineEvent(this.props.treeNode.pluginContext.event, 'rename', treeNode, { value }); this.cancelEdit(); }; @@ -68,8 +73,6 @@ export default class TreeTitle extends Component<{ } }; - private lastInput?: HTMLInputElement; - private setCaret = (input: HTMLInputElement | null) => { if (!input || this.lastInput === input) { return; @@ -80,13 +83,49 @@ export default class TreeTitle extends Component<{ // input.selectionStart = input.selectionEnd; }; + componentDidMount() { + const { treeNode } = this.props; + this.setState({ + editing: false, + title: treeNode.titleLabel, + condition: treeNode.condition, + visible: !treeNode.hidden, + }); + treeNode.onTitleLabelChanged(() => { + this.setState({ + title: treeNode.titleLabel, + }); + }); + treeNode.onConditionChanged(() => { + this.setState({ + condition: treeNode.condition, + }); + }); + treeNode.onHiddenChanged((hidden: boolean) => { + this.setState({ + visible: !hidden, + }); + }); + treeNode.onFilterResultChanged(() => { + const { filterWorking: newFilterWorking, keywords: newKeywords, matchSelf: newMatchSelf } = treeNode.filterReult; + this.setState({ filterWorking: newFilterWorking, keywords: newKeywords, matchSelf: newMatchSelf }); + }); + } + deleteClick = () => { + const { treeNode } = this.props; + const { node } = treeNode; + treeNode.deleteNode(node); + }; render() { const { treeNode, isModal } = this.props; - const { editing } = this.state; + const { pluginContext } = treeNode; + const { editing, filterWorking, matchSelf, keywords } = this.state; const isCNode = !treeNode.isRoot(); const { node } = treeNode; - const isNodeParent = node.isParental(); - const isContainer = node.isContainer(); + const { componentMeta } = node; + const availableActions = componentMeta ? componentMeta.availableActions.map((availableAction) => availableAction.name) : []; + const isNodeParent = node.isParentalNode; + const isContainer = node.isContainerNode; let style: any; if (isCNode) { const { depth } = treeNode; @@ -96,21 +135,27 @@ export default class TreeTitle extends Component<{ marginLeft: -indent, }; } - const { filterWorking, matchSelf, keywords } = treeNode.filterReult; - + const Extra = pluginContext.extraTitle; + const { intlNode, common, config } = pluginContext; + const { Tip, Title } = common.editorCabin; + const couldHide = availableActions.includes('hide'); + const couldLock = availableActions.includes('lock'); + const couldUnlock = availableActions.includes('unlock'); + const shouldShowHideBtn = isCNode && isNodeParent && !isModal && couldHide; + const shouldShowLockBtn = config.get('enableCanvasLock', false) && isContainer && isCNode && isNodeParent && ((couldLock && !node.isLocked) || (couldUnlock && node.isLocked)); + const shouldEditBtn = isCNode && isNodeParent; + const shouldDeleteBtn = isCNode && isNodeParent && node?.canPerformAction('remove'); return ( <div - className={classNames('tree-node-title', { - editing, - })} + className={classNames('tree-node-title', { editing })} style={style} - data-id={treeNode.id} + data-id={treeNode.nodeId} onClick={() => { if (isModal) { - if (node.getVisible()) { - node.document.modalNodesManager.setInvisible(node); + if (this.state.visible) { + node.document?.modalNodesManager?.setInvisible(node); } else { - node.document.modalNodesManager.setVisible(node); + node.document?.modalNodesManager?.setVisible(node); } return; } @@ -119,43 +164,46 @@ export default class TreeTitle extends Component<{ } }} > - {isModal && node.getVisible() && ( + {isModal && this.state.visible && ( <div onClick={() => { - node.document.modalNodesManager.setInvisible(node); + node.document?.modalNodesManager?.setInvisible(node); }} > <IconRadioActive className="tree-node-modal-radio-active" /> </div> )} - {isModal && !node.getVisible() && ( + {isModal && !this.state.visible && ( <div onClick={() => { - node.document.modalNodesManager.setVisible(node); + node.document?.modalNodesManager?.setVisible(node); }} > <IconRadio className="tree-node-modal-radio" /> </div> )} - {isCNode && <ExpandBtn treeNode={treeNode} />} + {isCNode && <ExpandBtn expandable={this.props.expandable} expanded={this.props.expanded} treeNode={treeNode} />} <div className="tree-node-icon">{createIcon(treeNode.icon)}</div> - <div className="tree-node-title-label" onDoubleClick={isNodeParent ? this.enableEdit : undefined}> + <div className="tree-node-title-label"> {editing ? ( <input className="tree-node-title-input" - defaultValue={treeNode.titleLabel} + defaultValue={this.state.title} onBlur={this.saveEdit} ref={this.setCaret} onKeyUp={this.handleKeyUp} /> ) : ( <Fragment> + {/* @ts-ignore */} <Title - title={treeNode.title} + title={this.state.title} match={filterWorking && matchSelf} keywords={keywords} /> + {Extra && <Extra node={treeNode?.node} />} {node.slotFor && ( <a className="tree-node-tag slot"> {/* todo: click redirect to prop */} + {/* @ts-ignore */} <Tip>{intlNode('Slot for {prop}', { prop: node.slotFor.key })}</Tip> </a> )} @@ -163,82 +211,140 @@ export default class TreeTitle extends Component<{ <a className="tree-node-tag loop"> {/* todo: click todo something */} <IconLoop /> + {/* @ts-ignore */} <Tip>{intlNode('Loop')}</Tip> </a> )} - {node.hasCondition() && !node.conditionGroup && ( + {this.state.condition && ( <a className="tree-node-tag cond"> {/* todo: click todo something */} <IconCond /> + {/* @ts-ignore */} <Tip>{intlNode('Conditional')}</Tip> </a> )} </Fragment> )} </div> - {isCNode && isNodeParent && !isModal && <HideBtn treeNode={treeNode} />} - {engineConfig.get('enableCanvasLock', false) && isContainer && isCNode && isNodeParent && <LockBtn treeNode={treeNode} />} + {shouldShowHideBtn && <HideBtn hidden={this.props.hidden} treeNode={treeNode} />} + {shouldShowLockBtn && <LockBtn locked={this.props.locked} treeNode={treeNode} />} + {shouldEditBtn && <RenameBtn treeNode={treeNode} onClick={this.enableEdit} />} + {shouldDeleteBtn && <DeleteBtn treeNode={treeNode} onClick={this.deleteClick} />} </div> ); } } -@observer -class LockBtn extends Component<{ treeNode: TreeNode }> { +class DeleteBtn extends PureComponent<{ + treeNode: TreeNode; + onClick: () => void; +}> { render() { - const { treeNode } = this.props; + const { intl, common } = this.props.treeNode.pluginContext; + const { Tip } = common.editorCabin; + return ( + <div + className="tree-node-delete-btn" + onClick={this.props.onClick} + > + <IconDelete /> + {/* @ts-ignore */} + <Tip>{intl('Delete')}</Tip> + </div> + ); + } +} + +class RenameBtn extends PureComponent<{ + treeNode: TreeNode; + onClick: (e: any) => void; +}> { + render() { + const { intl, common } = this.props.treeNode.pluginContext; + const { Tip } = common.editorCabin; + return ( + <div + className="tree-node-rename-btn" + onClick={this.props.onClick} + > + <IconSetting /> + {/* @ts-ignore */} + <Tip>{intl('Rename')}</Tip> + </div> + ); + } +} + +class LockBtn extends PureComponent<{ + treeNode: TreeNode; + locked: boolean; +}> { + render() { + const { treeNode, locked } = this.props; + const { intl, common } = this.props.treeNode.pluginContext; + const { Tip } = common.editorCabin; return ( <div className="tree-node-lock-btn" onClick={(e) => { e.stopPropagation(); - treeNode.setLocked(!treeNode.locked); + treeNode.setLocked(!locked); }} > - {treeNode.locked ? <IconUnlock /> : <IconLock /> } - <Tip>{treeNode.locked ? intl('Unlock') : intl('Lock')}</Tip> + {locked ? <IconUnlock /> : <IconLock /> } + {/* @ts-ignore */} + <Tip>{locked ? intl('Unlock') : intl('Lock')}</Tip> </div> ); } } -@observer -class HideBtn extends Component<{ treeNode: TreeNode }> { +class HideBtn extends PureComponent<{ + treeNode: TreeNode; + hidden: boolean; +}, { + hidden: boolean; +}> { render() { - const { treeNode } = this.props; + const { treeNode, hidden } = this.props; + const { intl, common } = treeNode.pluginContext; + const { Tip } = common.editorCabin; return ( <div className="tree-node-hide-btn" onClick={(e) => { e.stopPropagation(); - emitOutlineEvent(treeNode.hidden ? 'show' : 'hide', treeNode); - treeNode.setHidden(!treeNode.hidden); + emitOutlineEvent(treeNode.pluginContext.event, hidden ? 'show' : 'hide', treeNode); + treeNode.setHidden(!hidden); }} > - {treeNode.hidden ? <IconEye /> : <IconEyeClose />} - <Tip>{treeNode.hidden ? intl('Show') : intl('Hide')}</Tip> + {hidden ? <IconEye /> : <IconEyeClose />} + {/* @ts-ignore */} + <Tip>{hidden ? intl('Show') : intl('Hide')}</Tip> </div> ); } } - -@observer -class ExpandBtn extends Component<{ treeNode: TreeNode }> { +class ExpandBtn extends PureComponent<{ + treeNode: TreeNode; + expanded: boolean; + expandable: boolean; +}> { render() { - const { treeNode } = this.props; - if (!treeNode.expandable) { + const { treeNode, expanded, expandable } = this.props; + if (!expandable) { return <i className="tree-node-expand-placeholder" />; } return ( <div className="tree-node-expand-btn" onClick={(e) => { - if (treeNode.expanded) { + if (expanded) { e.stopPropagation(); } - emitOutlineEvent(treeNode.expanded ? 'collapse' : 'expand', treeNode); - treeNode.setExpanded(!treeNode.expanded); + emitOutlineEvent(treeNode.pluginContext.event, expanded ? 'collapse' : 'expand', treeNode); + treeNode.setExpanded(!expanded); }} > <IconArrowRight size="small" /> @@ -246,52 +352,3 @@ class ExpandBtn extends Component<{ treeNode: TreeNode }> { ); } } - -/* -interface Point { - clientX: number; - clientY: number; -} - -function setCaret(point: Point) { - debugger; - const range = getRangeFromPoint(point); - if (range) { - selectRange(range); - setTimeout(() => selectRange(range), 1); - } -} - -function getRangeFromPoint(point: Point): Range | undefined { - const x = point.clientX; - const y = point.clientY; - let range; - let pos: CaretPosition | null = null; - if (document.caretRangeFromPoint) { - range = document.caretRangeFromPoint(x, y); - } else if ((pos = document.caretPositionFromPoint(x, y))) { - range = document.createRange(); - range.setStart(pos.offsetNode, pos.offset); - range.collapse(true); - - } - return range; -} - -function selectRange(range: Range) { - const selection = document.getSelection(); - if (selection) { - selection.removeAllRanges(); - selection.addRange(range); - } -} - -function setCaretAfter(elem) { - const range = document.createRange(); - const node = elem.lastChild; - if (!node) return; - range.setStartAfter(node); - range.setEndAfter(node); - selectRange(range); -} -*/ diff --git a/packages/plugin-outline-pane/src/views/tree.tsx b/packages/plugin-outline-pane/src/views/tree.tsx index db99987d40..8428ec944c 100644 --- a/packages/plugin-outline-pane/src/views/tree.tsx +++ b/packages/plugin-outline-pane/src/views/tree.tsx @@ -1,9 +1,9 @@ -import { Component, MouseEvent as ReactMouseEvent } from 'react'; -import { observer, Editor, globalContext } from '@alilc/lowcode-editor-core'; -import { isRootNode, Node, DragObjectType, isShaken } from '@alilc/lowcode-designer'; -import { isFormEvent, canClickNode } from '@alilc/lowcode-utils'; -import { Tree } from '../tree'; -import RootTreeNodeView from './root-tree-node'; +import { MouseEvent as ReactMouseEvent, PureComponent } from 'react'; +import { isFormEvent, canClickNode, isShaken } from '@alilc/lowcode-utils'; +import { Tree } from '../controllers/tree'; +import TreeNodeView from './tree-node'; +import { IPublicEnumDragObjectType, IPublicModelNode } from '@alilc/lowcode-types'; +import TreeNode from '../controllers/tree-node'; function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string { let target: Element | null = e.target as Element; @@ -18,20 +18,29 @@ function getTreeNodeIdByEvent(e: ReactMouseEvent, stop: Element): null | string return (target as HTMLDivElement).dataset.id || null; } -@observer -export default class TreeView extends Component<{ tree: Tree }> { +export default class TreeView extends PureComponent<{ + tree: Tree; +}> { private shell: HTMLDivElement | null = null; - private hover(e: ReactMouseEvent) { - const { tree } = this.props; + private ignoreUpSelected = false; + + private boostEvent?: MouseEvent; + + state: { + root: TreeNode | null; + } = { + root: null, + }; - const doc = tree.document; - const { detecting } = doc.designer; - if (!detecting.enable) { + private hover(e: ReactMouseEvent) { + const { project } = this.props.tree.pluginContext; + const detecting = project.currentDocument?.detecting; + if (detecting?.enable) { return; } const node = this.getTreeNodeFromEvent(e)?.node; - detecting.capture(node || null); + node?.id && detecting?.capture(node.id); } private onClick = (e: ReactMouseEvent) => { @@ -54,31 +63,44 @@ export default class TreeView extends Component<{ tree: Tree }> { return; } - const { designer } = treeNode; - const doc = node.document; - const { selection, focusNode } = doc; + const { project, event, canvas } = this.props.tree.pluginContext; + const doc = project.currentDocument; + const selection = doc?.selection; + const focusNode = doc?.focusNode; const { id } = node; const isMulti = e.metaKey || e.ctrlKey || e.shiftKey; - designer.activeTracker.track(node); - if (isMulti && !node.contains(focusNode) && selection.has(id)) { + canvas.activeTracker?.track(node); + if (isMulti && focusNode && !node.contains(focusNode) && selection?.has(id)) { if (!isFormEvent(e.nativeEvent)) { selection.remove(id); } } else { - selection.select(id); - const editor = globalContext.get(Editor); - const selectedNode = designer.currentSelection?.getNodes()?.[0]; + selection?.select(id); + const selectedNode = selection?.getNodes()?.[0]; const npm = selectedNode?.componentMeta?.npm; const selected = [npm?.package, npm?.componentName].filter((item) => !!item).join('-') || selectedNode?.componentMeta?.componentName || ''; - editor?.emit('outlinePane.select', { + event.emit('outlinePane.select', { selected, }); } }; + private onDoubleClick = (e: ReactMouseEvent) => { + e.preventDefault(); + const treeNode = this.getTreeNodeFromEvent(e); + if (treeNode?.nodeId === this.state.root?.nodeId) { + return; + } + if (!treeNode?.expanded) { + this.props.tree.expandAllDecendants(treeNode); + } else { + this.props.tree.collapseAllDecendants(treeNode); + } + }; + private onMouseOver = (e: ReactMouseEvent) => { this.hover(e); }; @@ -96,10 +118,6 @@ export default class TreeView extends Component<{ tree: Tree }> { return tree.getTreeNodeById(id); } - private ignoreUpSelected = false; - - private boostEvent?: MouseEvent; - private onMouseDown = (e: ReactMouseEvent) => { if (isFormEvent(e.nativeEvent)) { return; @@ -114,36 +132,37 @@ export default class TreeView extends Component<{ tree: Tree }> { if (!canClickNode(node, e)) { return; } - - const { designer } = treeNode; - const doc = node.document; - const { selection, focusNode } = doc; + const { project, canvas } = this.props.tree.pluginContext; + const selection = project.currentDocument?.selection; + const focusNode = project.currentDocument?.focusNode; // TODO: shift selection const isMulti = e.metaKey || e.ctrlKey || e.shiftKey; const isLeftButton = e.button === 0; - if (isLeftButton && !node.contains(focusNode)) { - let nodes: Node[] = [node]; + if (isLeftButton && focusNode && !node.contains(focusNode)) { + let nodes: IPublicModelNode[] = [node]; this.ignoreUpSelected = false; if (isMulti) { // multi select mode, directily add - if (!selection.has(node.id)) { - designer.activeTracker.track(node); - selection.add(node.id); + if (!selection?.has(node.id)) { + canvas.activeTracker?.track(node); + selection?.add(node.id); this.ignoreUpSelected = true; } // todo: remove rootNodes id - selection.remove(focusNode.id); + selection?.remove(focusNode.id); // 获得顶层 nodes - nodes = selection.getTopNodes(); - } else if (selection.has(node.id)) { + if (selection) { + nodes = selection.getTopNodes(); + } + } else if (selection?.has(node.id)) { nodes = selection.getTopNodes(); } this.boostEvent = e.nativeEvent; - designer.dragon.boost( + canvas.dragon?.boost( { - type: DragObjectType.Node, + type: IPublicEnumDragObjectType.Node, nodes, }, this.boostEvent, @@ -152,15 +171,32 @@ export default class TreeView extends Component<{ tree: Tree }> { }; private onMouseLeave = () => { - const { tree } = this.props; - const doc = tree.document; - doc.designer.detecting.leave(doc); + const { pluginContext } = this.props.tree; + const { project } = pluginContext; + const doc = project.currentDocument; + doc?.detecting.leave(); }; - render() { + componentDidMount() { const { tree } = this.props; const { root } = tree; - if (!root) { + const { project } = tree.pluginContext; + this.setState({ root }); + const doc = project.currentDocument; + doc?.onFocusNodeChanged(() => { + this.setState({ + root: tree.root, + }); + }); + doc?.onImportSchema(() => { + this.setState({ + root: tree.root, + }); + }); + } + + render() { + if (!this.state.root) { return null; } return ( @@ -170,9 +206,14 @@ export default class TreeView extends Component<{ tree: Tree }> { onMouseDownCapture={this.onMouseDown} onMouseOver={this.onMouseOver} onClick={this.onClick} + onDoubleClick={this.onDoubleClick} onMouseLeave={this.onMouseLeave} > - <RootTreeNodeView key={root.id} treeNode={root} /> + <TreeNodeView + key={this.state.root?.id} + treeNode={this.state.root} + isRootNode + /> </div> ); } diff --git a/packages/rax-renderer/README.md b/packages/rax-renderer/README.md deleted file mode 100644 index 7b430de630..0000000000 --- a/packages/rax-renderer/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# Rax Renderer - -Rax 渲染模块。 - -## 安装 - -``` -$ npm install @alilc/lowcode-rax-renderer --save -``` - -## 使用 - -```js -import { createElement, render } from 'rax'; -import DriverUniversal from 'driver-universal'; -import RaxRenderer from '@ali/lowcode-rax-renderer'; - -const components = { - View, - Text -}; - -const schema = { - componentName: 'Page', - fileName: 'home', - children: [ - { - componentName: 'View', - children: [ - { - componentName: 'Text', - props: { - type: 'primary' - }, - children: ['Welcome to Your Rax App'] - } - ] - } - ] -}; - -render( - <RaxRenderer - schema={schema} - components={components} - />, - document.getElementById('root'), { driver: DriverUniversal } -); -``` diff --git a/packages/rax-renderer/build.json b/packages/rax-renderer/build.json deleted file mode 100644 index 3edf143801..0000000000 --- a/packages/rax-renderer/build.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "plugins": [ - [ - "build-plugin-rax-component", - { - "type": "rax", - "targets": ["web"] - } - ] - ] -} diff --git a/packages/rax-renderer/demo/index.jsx b/packages/rax-renderer/demo/index.jsx deleted file mode 100644 index cfcae2a201..0000000000 --- a/packages/rax-renderer/demo/index.jsx +++ /dev/null @@ -1,35 +0,0 @@ -import { createElement, render } from 'rax'; -import DriverUniversal from 'driver-universal'; -import View from 'rax-view'; -import Text from 'rax-text'; -import { Engine } from '../src/index'; - -const components = { - View, - Text, -}; - -const schema = { - componentName: 'Page', - fileName: 'home', - props: {}, - children: [ - { - componentName: 'View', - props: {}, - children: [ - { - componentName: 'Text', - props: { - type: 'primary', - }, - children: ['Welcome to Your Rax App!'], - }, - ], - }, - ], -}; - -render(<Engine schema={schema} components={components} />, document.getElementById('root'), { - driver: DriverUniversal, -}); diff --git a/packages/rax-renderer/demo/miniapp/app.js b/packages/rax-renderer/demo/miniapp/app.js deleted file mode 100644 index 3482935519..0000000000 --- a/packages/rax-renderer/demo/miniapp/app.js +++ /dev/null @@ -1 +0,0 @@ -App({}); diff --git a/packages/rax-renderer/demo/miniapp/app.json b/packages/rax-renderer/demo/miniapp/app.json deleted file mode 100644 index 94127c774c..0000000000 --- a/packages/rax-renderer/demo/miniapp/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pages": ["pages/index"], - "window": { - "defaultTitle": "demo" - } -} diff --git a/packages/rax-renderer/demo/miniapp/pages/index.axml b/packages/rax-renderer/demo/miniapp/pages/index.axml deleted file mode 100644 index 41b536b4ce..0000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.axml +++ /dev/null @@ -1 +0,0 @@ -<my-component></my-component> diff --git a/packages/rax-renderer/demo/miniapp/pages/index.js b/packages/rax-renderer/demo/miniapp/pages/index.js deleted file mode 100644 index 687d87e197..0000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.js +++ /dev/null @@ -1,4 +0,0 @@ -Page({ - onLoad() {}, - onShow() {}, -}); diff --git a/packages/rax-renderer/demo/miniapp/pages/index.json b/packages/rax-renderer/demo/miniapp/pages/index.json deleted file mode 100644 index 89b15c54ca..0000000000 --- a/packages/rax-renderer/demo/miniapp/pages/index.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "defaultTitle": "Miniapp Rax Text demo", - "usingComponents": { - "my-component": "../components/Target/index" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/app.js b/packages/rax-renderer/demo/wechat-miniprogram/app.js deleted file mode 100644 index 3482935519..0000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/app.js +++ /dev/null @@ -1 +0,0 @@ -App({}); diff --git a/packages/rax-renderer/demo/wechat-miniprogram/app.json b/packages/rax-renderer/demo/wechat-miniprogram/app.json deleted file mode 100644 index be00ced601..0000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pages": ["pages/index"], - "window": { - "title": "demo" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js deleted file mode 100644 index 687d87e197..0000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.js +++ /dev/null @@ -1,4 +0,0 @@ -Page({ - onLoad() {}, - onShow() {}, -}); diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json deleted file mode 100644 index 9448c84eaf..0000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "title": "Wechat MiniProgram Rax Text demo", - "usingComponents": { - "my-component": "../components/Target/index" - } -} diff --git a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml b/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml deleted file mode 100644 index 41b536b4ce..0000000000 --- a/packages/rax-renderer/demo/wechat-miniprogram/pages/index.wxml +++ /dev/null @@ -1 +0,0 @@ -<my-component></my-component> diff --git a/packages/rax-renderer/package.json b/packages/rax-renderer/package.json deleted file mode 100644 index 194d14602a..0000000000 --- a/packages/rax-renderer/package.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "name": "@alilc/lowcode-rax-renderer", - "version": "1.0.15", - "description": "Rax renderer for Ali lowCode engine", - "main": "lib/index.js", - "module": "es/index.js", - "miniappConfig": { - "main": "lib/miniapp/index", - "main:wechat": "lib/wechat-miniprogram/index" - }, - "files": [ - "dist", - "es", - "lib" - ], - "keywords": [ - "low-code", - "lowcode", - "Rax" - ], - "engines": { - "npm": ">=3.0.0" - }, - "peerDependencies": { - "prop-types": "^15.7.2", - "rax": "^1.1.0" - }, - "scripts": { - "start": "build-scripts start", - "build": "build-scripts build" - }, - "dependencies": { - "@alilc/lowcode-renderer-core": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", - "rax-find-dom-node": "^1.0.1" - }, - "devDependencies": { - "@alib/build-scripts": "^0.1.0", - "build-plugin-rax-component": "^0.2.11", - "driver-universal": "^3.1.3" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/rax-renderer" - }, - "license": "MIT", - "homepage": "https://unpkg.alibaba-inc.com/@alilc/lowcode-rax-renderer@0.1.2/build/index.html", - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" -} diff --git a/packages/rax-renderer/src/hoc/compFactory.tsx b/packages/rax-renderer/src/hoc/compFactory.tsx deleted file mode 100644 index 1c821ab6d9..0000000000 --- a/packages/rax-renderer/src/hoc/compFactory.tsx +++ /dev/null @@ -1,82 +0,0 @@ -// @ts-nocheck - -import { Component, forwardRef } from 'rax'; -import PropTypes from 'prop-types'; -import { AppHelper } from '@alilc/lowcode-utils'; -import { utils, contextFactory } from '@alilc/lowcode-renderer-core'; -import componentRendererFactory from '../renderer/component'; -import blockRendererFactory from '../renderer/block'; - -const { forEach, isFileSchema } = utils; - -export default function compFactory(schema, components = {}, componentsMap = {}, config = {}) { - // 自定义组件需要有自己独立的appHelper - const appHelper = new AppHelper(config); - const CompRenderer = componentRendererFactory(); - const BlockRenderer = blockRendererFactory(); - const AppContext = contextFactory(); - - class LNCompView extends Component { - static displayName = 'LceCompFactory'; - - static version = config.version || '0.0.0'; - - static contextType = AppContext; - - static propTypes = { - forwardedRef: PropTypes.func, - }; - - render() { - if (!schema || schema.componentName !== 'Component' || !isFileSchema(schema)) { - console.warn('自定义组件模型结构异常!'); - return null; - } - const { forwardedRef, ...otherProps } = this.props; - // 低代码组件透传应用上下文 - const ctx = ['utils', 'constants', 'history', 'location', 'match']; - ctx.forEach(key => { - if (!appHelper[key] && this.context?.appHelper && this.context?.appHelper[key]) { - appHelper.set(key, this.context.appHelper[key]); - } - }); - // 支持通过context透传国际化配置 - const localeProps = {}; - const { locale, messages } = this.context; - if (locale && messages && messages[schema.fileName]) { - localeProps.locale = locale; - localeProps.messages = messages[schema.fileName]; - } - const props = { - ...schema.defaultProps, - ...localeProps, - ...otherProps, - __schema: schema, - ref: forwardedRef, - }; - - return ( - <AppContext.Consumer> - {context => { - this.context = context; - return ( - <CompRenderer - {...props} - __appHelper={appHelper} - __components={{ ...components, Component: CompRenderer, Block: BlockRenderer }} - __componentsMap={componentsMap} - /> - ); - }} - </AppContext.Consumer> - ); - } - } - - const ResComp = forwardRef((props, ref) => <LNCompView {...props} forwardedRef={ref} />); - forEach(schema.static, (val, key) => { - ResComp[key] = val; - }); - ResComp.version = config.version || '0.0.0'; - return ResComp; -} diff --git a/packages/rax-renderer/src/index.ts b/packages/rax-renderer/src/index.ts deleted file mode 100644 index 2ea14ec1ae..0000000000 --- a/packages/rax-renderer/src/index.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Component, PureComponent, createElement, createContext, forwardRef } from 'rax'; -import findDOMNode from 'rax-find-dom-node'; -import { - adapter, - addonRendererFactory, - tempRendererFactory, - rendererFactory, -} from '@alilc/lowcode-renderer-core'; -import pageRendererFactory from './renderer/page'; -import componentRendererFactory from './renderer/component'; -import blockRendererFactory from './renderer/block'; -import CompFactory from './hoc/compFactory'; - -adapter.setRuntime({ - Component, - PureComponent, - createContext, - createElement, - forwardRef, - findDOMNode, -}); - -adapter.setRenderers({ - PageRenderer: pageRendererFactory(), - ComponentRenderer: componentRendererFactory(), - BlockRenderer: blockRendererFactory(), - AddonRenderer: addonRendererFactory(), - TempRenderer: tempRendererFactory(), -}); - -function factory() { - const Renderer = rendererFactory(); - return class extends Renderer { - constructor(props: any, context: any) { - super(props, context); - } - - isValidComponent(obj: any) { - return obj?.prototype?.setState || obj?.prototype instanceof Component; - } - }; -} - -const RaxRenderer = factory(); -const Engine = RaxRenderer; - -export { - Engine, - CompFactory, -}; - -export default RaxRenderer; diff --git a/packages/rax-renderer/src/renderer/block.tsx b/packages/rax-renderer/src/renderer/block.tsx deleted file mode 100644 index 2b2d6c93ad..0000000000 --- a/packages/rax-renderer/src/renderer/block.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { blockRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -export default function raxBlockRendererFactory() { - const OriginBlock = blockRendererFactory(); - return class BlockRenderer extends OriginBlock { - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '区块 schema 结构异常!'; - } - that.__debug(`render - ${__schema.fileName}`); - - const children = ((context) => { - that.context = context; - that.__generateCtx({}); - that.__render(); - return that.__renderComp((__components as any)?.Block, { blockContext: that }); - }); - return that.__renderContextConsumer(children); - } - }; -} diff --git a/packages/rax-renderer/src/renderer/component.tsx b/packages/rax-renderer/src/renderer/component.tsx deleted file mode 100644 index 6f221b3133..0000000000 --- a/packages/rax-renderer/src/renderer/component.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { componentRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -export default function raxComponentRendererFactory() { - const OriginComponent = componentRendererFactory(); - return class ComponentRenderer extends OriginComponent { - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '自定义组件 schema 结构异常!'; - } - that.__debug(`render - ${__schema.fileName}`); - - const { noContainer } = that.__parseData(__schema.props); - - const children = ((context) => { - that.context = context; - that.__generateCtx({ component: that }); - that.__render(); - // 传 null,使用内置的 div 来渲染,解决在页面中渲染 vc-component 报错的问题 - return that.__renderComp(null, { - compContext: that, - blockContext: that, - }); - }); - const content = that.__renderContextConsumer(children); - - if (noContainer) { - return content; - } - - return that.__renderContent(content); - } - }; -} diff --git a/packages/rax-renderer/src/renderer/page.tsx b/packages/rax-renderer/src/renderer/page.tsx deleted file mode 100644 index af8b78d882..0000000000 --- a/packages/rax-renderer/src/renderer/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { pageRendererFactory, types } from '@alilc/lowcode-renderer-core'; - -export default function raxPageRendererFactory() { - const OriginPage = pageRendererFactory(); - return class PageRenderer extends OriginPage { - async componentDidUpdate() { - // @ts-ignore - super.componentDidUpdate(...arguments); - } - - render() { - // @ts-ignore - const that: types.IRenderer = this; - const { __schema, __components } = that.props; - if (that.__checkSchema(__schema)) { - return '页面 schema 结构异常!'; - } - that.__debug(`render - ${__schema?.fileName}`); - - const { Page } = __components as any; - if (Page) { - const children = ((context) => { - that.context = context; - that.__render(); - return that.__renderComp(Page, { pageContext: that }); - }); - return that.__renderContextConsumer(children); - } - - return that.__renderContent(that.__renderContextConsumer((context) => { - that.context = context; - return that.__renderContextProvider({ pageContext: that }); - })); - } - }; -} diff --git a/packages/rax-renderer/tsconfig.json b/packages/rax-renderer/tsconfig.json deleted file mode 100644 index 7e264d1f05..0000000000 --- a/packages/rax-renderer/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015", "dom"], - "target": "esnext", - "module": "esnext", - "moduleResolution": "node", - "strict": false, - "strictPropertyInitialization": false, - "allowSyntheticDefaultImports": true, - "esModuleInterop": true, - "jsx": "react", - "jsxFactory": "createElement", - "importHelpers": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "sourceMap": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "outDir": "lib" - }, - "exclude": ["test", "lib", "es", "node_modules"], - "include": [ - "src" - ] -} diff --git a/packages/rax-simulator-renderer/.babelrc b/packages/rax-simulator-renderer/.babelrc deleted file mode 100644 index e0e2e5f343..0000000000 --- a/packages/rax-simulator-renderer/.babelrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "plugins": [ - ["@babel/plugin-transform-react-jsx", { - "pragma": "createElement", // default pragma is React.createElement - "pragmaFrag": "createFragment", // default is React.Fragment - "throwIfNamespace": false // defaults to true - }] - ] -} diff --git a/packages/rax-simulator-renderer/build.json b/packages/rax-simulator-renderer/build.json deleted file mode 100644 index b95a17aafe..0000000000 --- a/packages/rax-simulator-renderer/build.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "plugins": ["build-plugin-component", "./build.plugin.js"] -} diff --git a/packages/rax-simulator-renderer/build.plugin.js b/packages/rax-simulator-renderer/build.plugin.js deleted file mode 100644 index d613f1f56a..0000000000 --- a/packages/rax-simulator-renderer/build.plugin.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = ({ onGetWebpackConfig }) => { - onGetWebpackConfig((config) => { - config.performance.hints(false); - }); -}; diff --git a/packages/rax-simulator-renderer/build.umd.json b/packages/rax-simulator-renderer/build.umd.json deleted file mode 100644 index 833c92b246..0000000000 --- a/packages/rax-simulator-renderer/build.umd.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "entry": { - "rax-simulator-renderer": "src/index" - }, - "sourceMap": true, - "library": "___RaxSimulatorRenderer___", - "libraryTarget": "umd", - "externals": { - "react": "var window.React", - "react-dom": "var window.ReactDOM", - "prop-types": "var window.PropTypes", - "@alifd/next": "var Next", - "@alilc/lowcode-engine-ext": "var window.AliLowCodeEngineExt", - "rax": "var window.Rax", - "moment": "var moment", - "lodash": "var _" - }, - "polyfill": false, - "outputDir": "dist", - "vendor": false, - "ignoreHtmlTemplate": true, - "plugins": [ - "build-plugin-react-app", - [ - "build-plugin-fusion", - { - "externalNext": "umd" - } - ], - [ - "build-plugin-moment-locales", - { - "locales": ["zh-cn"] - } - ], - "./build.plugin.js" - ] -} diff --git a/packages/rax-simulator-renderer/package.json b/packages/rax-simulator-renderer/package.json deleted file mode 100644 index f69285cb86..0000000000 --- a/packages/rax-simulator-renderer/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "@alilc/lowcode-rax-simulator-renderer", - "version": "1.0.15", - "description": "rax simulator renderer for alibaba lowcode designer", - "main": "lib/index.js", - "module": "es/index.js", - "license": "MIT", - "files": [ - "dist" - ], - "scripts": { - "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --skip-demo", - "build:umd": "build-scripts build --config build.umd.json" - }, - "dependencies": { - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-rax-renderer": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", - "classnames": "^2.2.6", - "driver-universal": "^3.1.3", - "history": "^5.0.0", - "lodash": "^4.17.19", - "mobx": "^6.3.0", - "mobx-react": "^7.2.0", - "path-to-regexp": "3.2.0", - "rax-find-dom-node": "^1.0.0", - "react": "^16", - "react-dom": "^16.7.0" - }, - "devDependencies": { - "@alib/build-scripts": "^0.1.18", - "@babel/plugin-transform-react-jsx": "^7.10.4", - "@types/classnames": "^2.2.7", - "@types/node": "^13.7.1", - "@types/rax": "^1.0.0", - "@types/react": "^16", - "@types/react-dom": "^16", - "build-plugin-component": "^0.2.11", - "build-plugin-rax-component": "^0.2.11" - }, - "peerDependencies": { - "rax": "^1.1.0" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "http", - "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/rax-simulator-renderer" - }, - "homepage": "https://unpkg.alibaba-inc.com/@alilc/lowcode-rax-simulator-renderer@1.0.73/build/index.html", - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" -} diff --git a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx b/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx deleted file mode 100644 index 6608942c4b..0000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/UnusualComponent/index.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Component } from 'rax'; -import lg from '@ali/vu-logger'; - -import './index.less'; - -export class UnknownComponent extends Component { - props: { - _componentName: string; - }; - - render() { - lg.log('ERROR_NO_COMPONENT_VIEW'); - lg.error('Error component information:', this.props); - return <div className="engine-unknow-component">组件 {this.props._componentName} 无视图,请打开控制台排查</div>; - } -} - -export class FaultComponent extends Component { - props: { - _componentName: string; - }; - - render() { - return <div className="engine-fault-component">组件 {this.props._componentName} 渲染错误,请打开控制台排查</div>; - } -} - -export class HiddenComponent extends Component { - render() { - return <div className="engine-hidden-component">在本页面不显示</div>; - } -} - -export default { FaultComponent, HiddenComponent, UnknownComponent }; diff --git a/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx b/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx deleted file mode 100644 index 5276b0018d..0000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/leaf.tsx +++ /dev/null @@ -1,251 +0,0 @@ -import { Component } from 'rax'; - -class Leaf extends Component { - static displayName = 'Leaf'; - - static componentMetadata = { - componentName: 'Leaf', - configure: { - props: [{ - name: 'children', - setter: 'StringSetter', - }], - // events/className/style/general/directives - supports: false, - }, - }; - - render() { - const { children } = this.props; - return children; - } -} - -export default Leaf; - -// import { Component, createElement } from 'rax'; -// import findDOMNode from 'rax-find-dom-node'; -// import { each, get, omit } from 'lodash'; -// import { getView, setNativeNode, createNodeStyleSheet } from '../renderUtils'; - -// import { FaultComponent, HiddenComponent, UnknownComponent } from '../UnusualComponent'; - -// export interface ILeaf { -// leaf: any; -// } -// export default class Leaf extends Component<ILeaf, {}> { -// static displayName = 'Leaf'; - -// state = { -// hasError: false, -// }; - -// willDetach: any[]; - -// styleSheet: any; - -// context: any; -// refs: any; - -// componentWillMount() { -// const { leaf } = this.props; -// this.willDetach = [ -// leaf.onPropsChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }), -// leaf.onChildrenChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }), -// leaf.onStatusChange((status: { dropping: boolean }, field: string) => { -// // console.log({...status}, field) -// if (status.dropping !== false) { -// // 当 dropping 为 Insertion 对象时,强制渲染会出错,原因待查 -// return; -// } -// if (field === 'dragging' || field === 'dropping' || field === 'pseudo' || field === 'visibility') { -// // 强制刷新 -// this.setState(this.state); -// } -// }), -// ]; - -// /** -// * while props replaced -// * bind the new event on it -// */ -// leaf.onPropsReplace(() => { -// this.willDetach[0](); -// this.willDetach[0] = leaf.onPropsChange(() => { -// // 强制刷新 -// this.setState(this.state); -// }); -// }); -// } - -// componentDidMount() { -// this.modifyDOM(); -// } - -// shouldComponentUpdate() { -// // forceUpdate 的替代方案 -// return true; -// // const pageCanRefresh = this.leaf.getPage().canRefresh(); -// // if (pageCanRefresh) { -// // return pageCanRefresh; -// // } -// // const getExtProps = obj => { -// // const { leaf, ...props } = obj; -// // return props; -// // }; -// // return !shallowEqual(getExtProps(this.props), getExtProps(nextProps)); -// } - -// componentDidUpdate() { -// this.modifyDOM(); -// } - -// componentWillUnmount() { -// if (this.willDetach) { -// this.willDetach.forEach((off) => off()); -// } -// setNativeNode(this.props.leaf, null); -// } - -// componentDidCatch() { -// this.setState({ hasError: true }, () => { -// console.log('error'); -// }); -// } - -// modifyDOM() { -// const shell = findDOMNode(this); -// const { leaf } = this.props; -// // 与 React 不同,rax 的 findDOMNode 找不到节点时, -// // shell 会是 <!-- empty -->,而不是 null, -// // 所以这里进行是否为注释的判断 -// if (shell && shell.nodeType !== window.Node.COMMENT_NODE) { -// setNativeNode(leaf, shell); -// if (leaf.getStatus('dragging')) { -// get(shell, 'classList').add('engine-dragging'); -// } else { -// get(shell, 'classList').remove('engine-dragging'); -// } -// each(get(shell, 'classList'), (cls) => { -// if (cls.substring(0, 8) === '-pseudo-') { -// get(shell, 'classList').remove(cls); -// } -// }); -// const pseudo = leaf.getStatus('pseudo'); -// if (pseudo) { -// get(shell, 'classList').add(`-pseudo-${pseudo}`); -// } -// } else { -// setNativeNode(leaf, null); -// } -// } - -// render() { -// const props = omit(this.props, ['leaf']); -// const { leaf } = this.props; -// const componentName = leaf.getComponentName(); - -// const View = getView(componentName); - -// const newProps = { -// _componentName: componentName, -// }; - -// if (!View) { -// return createElement(UnknownComponent, { -// // _componentName: componentName, -// ...newProps, -// }); -// } - -// let staticProps = { -// ...leaf.getStaticProps(false), -// ...props, -// _componentName: componentName, -// _leaf: leaf, -// componentId: leaf.getId(), -// }; - -// if (!leaf.isVisibleInPane()) { -// return null; -// } - -// if (!leaf.isVisible()) { -// return createElement(HiddenComponent, { -// ...staticProps, -// }); -// } - -// if (this.state.hasError) { -// return createElement(FaultComponent, { -// // _componentName: componentName, -// ...newProps, -// }); -// } - -// if (this.styleSheet) { -// this.styleSheet.parentNode.removeChild(this.styleSheet); -// } - -// this.styleSheet = createNodeStyleSheet(staticProps); - -// if (leaf.ableToModifyChildren()) { -// const children = leaf -// .getChildren() -// .filter((child: any) => child.getComponentName() !== 'Slot') -// .map((child: any) => -// createElement(Leaf, { -// key: child.getId(), -// leaf: child, -// }), -// ); -// // const insertion = leaf.getStatus('dropping'); -// // InsertionGhost 都是React节点,用Rax渲染会报错,后面这些节点需要通过Rax组件来实现 -// // if (children.length < 1 && insertion && insertion.getIndex() !== null) { - -// // //children = []; -// // children = [<InsertionGhost key="insertion" />]; -// // } else if (insertion && insertion.isNearEdge()) { -// // if (insertion.isNearAfter()) { -// // children.push(<InsertionGhost key="insertion" />); -// // } else { -// // children.unshift(<InsertionGhost key="insertion" />); -// // } -// // } -// staticProps = { -// ...staticProps, -// ...this.processSlots(this.props.leaf.getChildren()), -// }; - -// return createElement( -// View, -// { -// ...staticProps, -// }, -// children, -// ); -// } - -// return createElement(View, { -// ...staticProps, -// }); -// } - -// processSlots(children: Rax.RaxNodeArray) { -// const slots: any = {}; -// children && -// children.length && -// children.forEach((child: any) => { -// if (child.getComponentName() === 'Slot') { -// slots[child.getPropValue('slotName')] = <Leaf key={child.getId()} leaf={child} />; -// } -// }); -// return slots; -// } -// } diff --git a/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts b/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts deleted file mode 100644 index 10f8438fb2..0000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/renderUtils.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { isObject } from 'lodash'; -import { css } from '@alilc/lowcode-utils'; - -const { toCss } = css; -const engine = (window as any).VisualEngine; -const { Trunk, Viewport } = engine; - -export const NativeNodeCache: any = {}; - -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} - -export function shallowEqual(obj: { [key: string]: string }, tObj: { [key: string]: string }) { - for (const i in obj) { - if (Object.prototype.hasOwnProperty.call(obj, i) && obj[i] !== tObj[i]) { - return false; - } - } - return true; -} - -export function createNodeStyleSheet(props: any) { - if (props && props.fieldId) { - let styleProp = props.__style__; - - if (isObject(styleProp)) { - styleProp = toCss(styleProp); - } - - if (typeof styleProp === 'string') { - const s = document.createElement('style'); - const cssId = `_style_pesudo_${ props.fieldId}`; - const cssClass = `_css_pesudo_${ props.fieldId}`; - - props.className = cssClass; - s.setAttribute('type', 'text/css'); - s.setAttribute('id', cssId); - document.getElementsByTagName('head')[0].appendChild(s); - - s.appendChild( - document.createTextNode( - styleProp - .replace(/(\d+)rpx/g, (a, b) => { - return `${b / 2}px`; - }) - .replace(/:root/g, `.${ cssClass}`), - ), - ); - return s; - } - } -} - -export function setNativeNode(leaf: any, node: Rax.RaxNode) { - const id = leaf.getId(); - if (NativeNodeCache[id] === node) { - return; - } - NativeNodeCache[id] = node; - leaf.mountChange(); -} - -export function getView(componentName: string) { - // let view = new Trunk().getPrototypeView(componentName); - let view = Trunk.getPrototypeView(componentName); - if (!view) { - return null; - } - const viewport = Viewport.getViewport(); - if (viewport) { - const [mode, device] = viewport.split('-', 2).map(ucfirst); - if (view.hasOwnProperty(device)) { - view = view[device]; - } - - if (view.hasOwnProperty(mode)) { - view = view[mode]; - } - } - - return view; -} diff --git a/packages/rax-simulator-renderer/src/builtin-components/slot.tsx b/packages/rax-simulator-renderer/src/builtin-components/slot.tsx deleted file mode 100644 index 3a77491bc0..0000000000 --- a/packages/rax-simulator-renderer/src/builtin-components/slot.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { Component } from 'rax'; - -class Slot extends Component { - static displayName = 'Slot'; - - static componentMetadata = { - componentName: 'Slot', - configure: { - props: [{ - name: '___title', - title: { - type: 'i18n', - 'en-US': 'Slot Title', - 'zh-CN': '插槽标题', - }, - setter: 'StringSetter', - defaultValue: '插槽容器', - }, { - name: '___params', - title: { - type: 'i18n', - 'en-US': 'Slot Params', - 'zh-CN': '插槽入参', - }, - setter: { - componentName: 'ArraySetter', - props: { - itemSetter: { - componentName: 'StringSetter', - props: { - placeholder: { - type: 'i18n', - 'zh-CN': '参数名称', - 'en-US': 'Argument Name', - }, - }, - }, - }, - }, - }], - // events/className/style/general/directives - supports: false, - }, - }; - - render() { - const { children } = this.props; - return ( - <div className="lc-container">{children}</div> - ); - } -} - -export default Slot; diff --git a/packages/rax-simulator-renderer/src/host.ts b/packages/rax-simulator-renderer/src/host.ts deleted file mode 100644 index c5cf2e3e1c..0000000000 --- a/packages/rax-simulator-renderer/src/host.ts +++ /dev/null @@ -1,4 +0,0 @@ -// NOTE: 仅做类型标注,切勿做其它用途 -import { BuiltinSimulatorHost } from '@alilc/lowcode-designer'; - -export const host: BuiltinSimulatorHost = (window as any).LCSimulatorHost; diff --git a/packages/rax-simulator-renderer/src/image.d.ts b/packages/rax-simulator-renderer/src/image.d.ts deleted file mode 100644 index 7ed4ad925c..0000000000 --- a/packages/rax-simulator-renderer/src/image.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module 'rax-find-dom-node'; -declare module '@alilc/lowcode-rax-renderer/lib/index'; diff --git a/packages/rax-simulator-renderer/src/index.ts b/packages/rax-simulator-renderer/src/index.ts deleted file mode 100644 index 3a88726657..0000000000 --- a/packages/rax-simulator-renderer/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import renderer from './renderer'; - -if (typeof window !== 'undefined') { - (window as any).SimulatorRenderer = renderer; -} - -export default renderer; diff --git a/packages/rax-simulator-renderer/src/rax-use-router.js b/packages/rax-simulator-renderer/src/rax-use-router.js deleted file mode 100644 index 9a399a9478..0000000000 --- a/packages/rax-simulator-renderer/src/rax-use-router.js +++ /dev/null @@ -1,288 +0,0 @@ -// Inspired by react-router and universal-router -import { useState, useEffect, useLayoutEffect, createElement } from 'rax'; -import pathToRegexp from 'path-to-regexp'; - -const cache = {}; -function decodeParam(val) { - try { - return decodeURIComponent(val); - } catch (err) { - return val; - } -} - -function matchPath(route, pathname, parentParams) { - let { path, routes, exact: end = true, strict = false, sensitive = false } = route; - // If not has path or has routes that should do not exact match - if (path == null || routes) { - end = false; - } - - // Default path is empty - path = path || ''; - - const regexpCacheKey = `${path}|${end}|${strict}|${sensitive}`; - const keysCacheKey = `${regexpCacheKey }|`; - - let regexp = cache[regexpCacheKey]; - const keys = cache[keysCacheKey] || []; - - if (!regexp) { - regexp = pathToRegexp(path, keys, { - end, - strict, - sensitive, - }); - cache[regexpCacheKey] = regexp; - cache[keysCacheKey] = keys; - } - - const result = regexp.exec(pathname); - if (!result) { - return null; - } - - const url = result[0]; - const params = { ...parentParams, history: router.history, location: router.history.location }; - - for (let i = 1; i < result.length; i++) { - const key = keys[i - 1]; - const prop = key.name; - const value = result[i]; - if (value !== undefined || !Object.prototype.hasOwnProperty.call(params, prop)) { - if (key.repeat) { - params[prop] = value ? value.split(key.delimiter).map(decodeParam) : []; - } else { - params[prop] = value ? decodeParam(value) : value; - } - } - } - - return { - path: !end && url.charAt(url.length - 1) === '/' ? url.slice(1) : url, - params, - }; -} - -function matchRoute(route, baseUrl, pathname, parentParams) { - let matched; - let childMatches; - let childIndex = 0; - - return { - next() { - if (!matched) { - matched = matchPath(route, pathname, parentParams); - - if (matched) { - return { - done: false, - $: { - route, - baseUrl, - path: matched.path, - params: matched.params, - }, - }; - } - } - - if (matched && route.routes) { - while (childIndex < route.routes.length) { - if (!childMatches) { - const childRoute = route.routes[childIndex]; - childRoute.parent = route; - - childMatches = matchRoute( - childRoute, - baseUrl + matched.path, - pathname.slice(matched.path.length), - matched.params, - ); - } - - const childMatch = childMatches.next(); - if (!childMatch.done) { - return { - done: false, - $: childMatch.$, - }; - } - - childMatches = null; - childIndex++; - } - } - - return { done: true }; - }, - }; -} - -let _initialized = false; -let _routerConfig = null; -const router = { - history: null, - handles: [], - errorHandler() { }, - addHandle(handle) { - return router.handles.push(handle); - }, - removeHandle(handleId) { - router.handles[handleId - 1] = null; - }, - triggerHandles(component) { - router.handles.forEach((handle) => { - handle && handle(component); - }); - }, - match(fullpath) { - if (fullpath == null) return; - - router.fullpath = fullpath; - - const parent = router.root; - const matched = matchRoute( - parent, - parent.path, - fullpath, - ); - - function next(parent) { - const current = matched.next(); - - if (current.done) { - const error = new Error(`No match for ${fullpath}`); - return router.errorHandler(error, router.history.location); - } - - let { component } = current.$.route; - if (typeof component === 'function') { - component = component(current.$.params, router.history.location); - } - if (component instanceof Promise) { - // Lazy loading component by import('./Foo') - return component.then((component) => { - // Check current fullpath avoid router has changed before lazy loading complete - if (fullpath === router.fullpath) { - router.triggerHandles(component); - } - }); - } else if (component != null) { - router.triggerHandles(component); - return component; - } else { - return next(parent); - } - } - - return next(parent); - }, -}; - -function matchLocation({ pathname }) { - router.match(pathname); -} - - -function getInitialComponent(routerConfig) { - let InitialComponent = []; - - if (_routerConfig === null) { - if (process.env.NODE_ENV !== 'production') { - if (!routerConfig) { - throw new Error('Error: useRouter should have routerConfig, see: https://www.npmjs.com/package/rax-use-router.'); - } - if (!routerConfig.history || !routerConfig.routes) { - throw new Error('Error: routerConfig should contain history and routes, see: https://www.npmjs.com/package/rax-use-router.'); - } - } - _routerConfig = routerConfig; - } - if (_routerConfig.InitialComponent) { - InitialComponent = _routerConfig.InitialComponent; - } - router.history = _routerConfig.history; - - return InitialComponent; -} - -let unlisten = null; -let handleId = null; -let pathes = ''; -export function useRouter(routerConfig) { - const [component, setComponent] = useState(getInitialComponent(routerConfig)); - - let newPathes = ''; - if (routerConfig) { - _routerConfig = routerConfig; - const { routes } = _routerConfig; - router.root = Array.isArray(routes) ? { routes } : routes; - if (Array.isArray(routes)) { - newPathes = routes.map(it => it.path).join(','); - } else { - newPathes = routes.path; - } - } - if (_initialized && _routerConfig.history) { - if (newPathes !== pathes) { - matchLocation(_routerConfig.history.location); - pathes = newPathes; - } - } - - useLayoutEffect(() => { - if (unlisten) { - unlisten(); - unlisten = null; - } - - if (handleId) { - router.removeHandle(handleId); - handleId = null; - } - - const { history } = _routerConfig; - const { routes } = _routerConfig; - - router.root = Array.isArray(routes) ? { routes } : routes; - - handleId = router.addHandle((component) => { - setComponent(component); - }); - - // Init path match - if (_initialized || !_routerConfig.InitialComponent) { - matchLocation(history.location); - pathes = newPathes; - } - - unlisten = history.listen(({ location }) => { - matchLocation(location); - pathes = newPathes; - }); - - _initialized = true; - - return () => { - pathes = ''; - router.removeHandle(handleId); - handleId = null; - unlisten(); - unlisten = null; - }; - }, []); - - return { component }; -} - -export function withRouter(Component) { - function Wrapper(props) { - const { history } = router; - return createElement(Component, { ...props, history, location: history.location }); - } - - Wrapper.displayName = `withRouter(${ Component.displayName || Component.name })`; - Wrapper.WrappedComponent = Component; - return Wrapper; -} diff --git a/packages/rax-simulator-renderer/src/renderer-view.tsx b/packages/rax-simulator-renderer/src/renderer-view.tsx deleted file mode 100644 index 2e8d6e0fcd..0000000000 --- a/packages/rax-simulator-renderer/src/renderer-view.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import RaxRenderer from '@alilc/lowcode-rax-renderer'; -import { History } from 'history'; -import { Component, createElement, Fragment } from 'rax'; -import { useRouter } from './rax-use-router'; -import { DocumentInstance, SimulatorRendererContainer } from './renderer'; -import './renderer.less'; -import { uniqueId } from '@alilc/lowcode-utils'; -import { GlobalEvent } from '@alilc/lowcode-types'; -import { host } from './host'; - -// patch cloneElement avoid lost keyProps -const originCloneElement = (window as any).Rax.cloneElement; -(window as any).Rax.cloneElement = (child: any, { _leaf, ...props }: any = {}, ...rest: any[]) => { - if (child.ref && props.ref) { - const dRef = props.ref; - const cRef = child.ref; - props.ref = (x: any) => { - if (cRef) { - if (typeof cRef === 'function') { - cRef(x); - } else { - try { - cRef.current = x; - } catch (e) { - console.error(e); - } - } - } - if (dRef) { - if (typeof dRef === 'function') { - dRef(x); - } else { - try { - dRef.current = x; - } catch (e) { - console.error(e); - } - } - } - }; - } - return originCloneElement(child, props, ...rest); -}; - -export default class SimulatorRendererView extends Component<{ rendererContainer: SimulatorRendererContainer }> { - private unlisten: any; - - componentDidMount() { - const { rendererContainer } = this.props; - this.unlisten = rendererContainer.onLayoutChange(() => { - this.forceUpdate(); - }); - } - - componentWillUnmount() { - if (this.unlisten) { - this.unlisten(); - } - } - - render() { - const { rendererContainer } = this.props; - return ( - <Layout rendererContainer={rendererContainer}> - <Routes rendererContainer={rendererContainer} history={rendererContainer.history} /> - </Layout> - ); - } -} - -export const Routes = (props: { - rendererContainer: SimulatorRendererContainer; - history: History; -}) => { - const { rendererContainer, history } = props; - const { documentInstances } = rendererContainer; - - const routes = { - history, - routes: documentInstances.map(instance => { - return { - path: instance.path, - component: (props: any) => <Renderer key={instance.id} rendererContainer={rendererContainer} documentInstance={instance} {...props} />, - }; - }), - }; - const { component } = useRouter(routes); - return component; -}; - -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} -function getDeviceView(view: any, device: string, mode: string) { - if (!view || typeof view === 'string') { - return view; - } - - // compatible vision Mobile | Preview - device = ucfirst(device); - if (device === 'Mobile' && view.hasOwnProperty(device)) { - view = view[device]; - } - mode = ucfirst(mode); - if (mode === 'Preview' && view.hasOwnProperty(mode)) { - view = view[mode]; - } - return view; -} - -class Layout extends Component<{ rendererContainer: SimulatorRendererContainer }> { - constructor(props: any) { - super(props); - this.props.rendererContainer.onReRender(() => { - this.forceUpdate(); - }); - } - - render() { - const { rendererContainer, children } = this.props; - const { layout } = rendererContainer; - - if (layout) { - const { Component, props, componentName } = layout; - if (Component) { - return <Component props={props}>{children}</Component>; - } - if (componentName && rendererContainer.getComponent(componentName)) { - return createElement( - rendererContainer.getComponent(componentName), - { - ...props, - rendererContainer, - }, - [children], - ); - } - } - - return <Fragment>{children}</Fragment>; - } -} - -class Renderer extends Component<{ - rendererContainer: SimulatorRendererContainer; - documentInstance: DocumentInstance; -}> { - private unlisten: any; - private key: string; - private startTime: number | null = null; - - componentWillMount() { - this.key = uniqueId('renderer'); - } - - componentDidMount() { - const { documentInstance } = this.props; - this.unlisten = documentInstance.onReRender((params) => { - if (params && params.shouldRemount) { - this.key = uniqueId('renderer'); - } - this.forceUpdate(); - }); - } - - componentWillUnmount() { - if (this.unlisten) { - this.unlisten(); - } - } - shouldComponentUpdate() { - return false; - } - - componentDidUpdate() { - if (this.startTime) { - const time = Date.now() - this.startTime; - const nodeCount = host.designer.currentDocument?.getNodeCount?.(); - host.designer.editor?.emit(GlobalEvent.Node.Rerender, { - componentName: 'Renderer', - type: 'All', - time, - nodeCount, - }); - } - } - - schemaChangedSymbol = false; - - getSchemaChangedSymbol = () => { - return this.schemaChangedSymbol; - }; - - setSchemaChangedSymbol = (symbol: boolean) => { - this.schemaChangedSymbol = symbol; - }; - - render() { - const { documentInstance } = this.props; - const { container, document } = documentInstance; - const { designMode, device } = container; - const { rendererContainer: renderer } = this.props; - this.startTime = Date.now(); - this.schemaChangedSymbol = false; - - return ( - <RaxRenderer - schema={documentInstance.schema} - components={renderer.components} - appHelper={renderer.context} - context={renderer.context} - device={device} - designMode={renderer.designMode} - key={this.key} - __host={host} - __container={container} - suspended={documentInstance.suspended} - self={documentInstance.scope} - onCompGetRef={(schema: any, ref: any) => { - documentInstance.mountInstance(schema.id, ref); - }} - thisRequiredInJSE={host.thisRequiredInJSE} - documentId={document.id} - getNode={(id: string) => documentInstance.getNode(id) as any} - rendererName="PageRenderer" - customCreateElement={(Component: any, props: any, children: any) => { - const { __id, ...viewProps } = props; - viewProps.componentId = __id; - const leaf = documentInstance.getNode(__id); - viewProps._leaf = leaf; - viewProps._componentName = leaf?.componentName; - // 如果是容器 && 无children && 高宽为空 增加一个占位容器,方便拖动 - if ( - !viewProps.dataSource && - leaf?.isContainer() && - (children == null || (Array.isArray(children) && !children.length)) && - (!viewProps.style || Object.keys(viewProps.style).length === 0) - ) { - children = ( - <div className="lc-container-placeholder" style={viewProps.placeholderStyle}> - {viewProps.placeholder || '拖拽组件或模板到这里'} - </div> - ); - } - - // if (viewProps._componentName === 'Menu') { - // Object.assign(viewProps, { - // _componentName: 'Menu', - // className: '_css_pesudo_menu_kbrzyh0f', - // context: { VE: (window as any).VisualLowCodeRenderer }, - // direction: undefined, - // events: { ignored: true }, - // fieldId: 'menu_kbrzyh0f', - // footer: '', - // header: '', - // mode: 'inline', - // onItemClick: { ignored: true }, - // onSelect: { ignored: true }, - // popupAlign: 'follow', - // selectMode: false, - // triggerType: 'click', - // }); - // console.info('menuprops', viewProps); - // } - - return createElement( - getDeviceView(Component, device, designMode), - viewProps, - leaf?.isContainer() ? (children == null ? [] : Array.isArray(children) ? children : [children]) : children, - ); - }} - /> - ); - } -} diff --git a/packages/rax-simulator-renderer/src/renderer.less b/packages/rax-simulator-renderer/src/renderer.less deleted file mode 100644 index b71dde9896..0000000000 --- a/packages/rax-simulator-renderer/src/renderer.less +++ /dev/null @@ -1,125 +0,0 @@ -body, html { - display: block; - background: white; - padding: 0; - margin: 0; -} - -html.engine-cursor-move, html.engine-cursor-move * { - cursor: grabbing !important; -} - -html.engine-cursor-copy, html.engine-cursor-copy * { - cursor: copy !important; -} - -html.engine-cursor-ew-resize, html.engine-cursor-ew-resize * { - cursor: ew-resize !important; -} - -::-webkit-scrollbar { - display: none; -} - -.lc-container { - &:empty { - background: #f2f3f5; - color: #a7b1bd; - outline: 1px dashed rgba(31, 56, 88, 0.2); - outline-offset: -1px !important; - height: 66px; - max-height: 100%; - min-width: 140px; - text-align: center; - overflow: hidden; - display: flex; - align-items: center; - &:before { - content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC'; - font-size: 14px; - z-index: 1; - width: 100%; - white-space: nowrap; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - } - } -} - -.engine-empty { - background: #f2f3f5; - color: #a7b1bd; - outline: 1px dashed rgba(31, 56, 88, 0.2); - outline-offset: -1px !important; - height: 66px; - max-height: 100%; - min-width: 140px; - text-align: center; - overflow: hidden; - display: flex; - align-items: center; -} - -.engine-empty:before { - content: '\62D6\62FD\7EC4\4EF6\6216\6A21\677F\5230\8FD9\91CC'; - font-size: 14px; - z-index: 1; - width: 100%; - white-space: nowrap; - height: 100%; - display: flex; - align-items: center; - justify-content: center; -} - -.lc-container-placeholder { - min-height: 60px; - height: 100%; - width: 100%; - background-color: rgb(240, 240, 240); - border: 1px dotted; - color: rgb(167, 177, 189); - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; -} - -body.engine-document { - &:after, &:before { - content: ""; - display: table; - } - &:after { - clear: both; - } - - /* - .next-input-group, - .next-checkbox-group,.next-date-picker,.next-input,.next-month-picker, - .next-number-picker,.next-radio-group,.next-range,.next-range-picker, - .next-rating,.next-select,.next-switch,.next-time-picker,.next-upload, - .next-year-picker, - .next-breadcrumb-item,.next-calendar-header,.next-calendar-table { - pointer-events: none !important; - } */ -} - -.engine-live-editing { - cursor: text; - outline: none; - box-shadow: 0 0 0 2px rgb(102, 188, 92); - user-select: text; -} - -/* stylelint-disable-next-line selector-max-id */ -#app { - height: 100vh; -} - - -.luna-page { - height: 100%; -} diff --git a/packages/rax-simulator-renderer/src/renderer.ts b/packages/rax-simulator-renderer/src/renderer.ts deleted file mode 100644 index a123dfc7e3..0000000000 --- a/packages/rax-simulator-renderer/src/renderer.ts +++ /dev/null @@ -1,663 +0,0 @@ -import { BuiltinSimulatorRenderer, Component, DocumentModel, Node, NodeInstance } from '@alilc/lowcode-designer'; -import { ComponentSchema, NodeSchema, NpmInfo, RootSchema, TransformStage } from '@alilc/lowcode-types'; -import { Asset, compatibleLegaoSchema, cursor, isElement, isESModule, isPlainObject, isReactComponent, setNativeSelection } from '@alilc/lowcode-utils'; -import LowCodeRenderer from '@alilc/lowcode-rax-renderer'; -import { computed, observable as obx, untracked, makeObservable, configure } from 'mobx'; -import DriverUniversal from 'driver-universal'; -import { EventEmitter } from 'events'; -import { createMemoryHistory, MemoryHistory } from 'history'; -// @ts-ignore -import Rax, { ComponentType, createElement, render as raxRender, shared } from 'rax'; -import Leaf from './builtin-components/leaf'; -import Slot from './builtin-components/slot'; -import { host } from './host'; -import SimulatorRendererView from './renderer-view'; -import { raxFindDOMNodes } from './utils/find-dom-nodes'; -import { getClientRects } from './utils/get-client-rects'; -import loader from './utils/loader'; -import { parseQuery, withQueryParams } from './utils/url'; - -configure({ enforceActions: 'never' }); -const { Instance } = shared; - -export interface LibraryMap { - [key: string]: string; -} - -const SYMBOL_VNID = Symbol('_LCNodeId'); -const SYMBOL_VDID = Symbol('_LCDocId'); - -const INTERNAL = '_internal'; - -function accessLibrary(library: string | object) { - if (typeof library !== 'string') { - return library; - } - - return (window as any)[library]; -} - -// Slot/Leaf and Fragment|FunctionComponent polyfill(ref) - -const builtinComponents = { - Slot, - Leaf, -}; - -function buildComponents( - libraryMap: LibraryMap, - componentsMap: { [componentName: string]: NpmInfo | ComponentType<any> | ComponentSchema }, - createComponent: (schema: ComponentSchema) => Component | null, -) { - const components: any = { - ...builtinComponents, - }; - Object.keys(componentsMap).forEach((componentName) => { - let component = componentsMap[componentName]; - if (component && (component as ComponentSchema).componentName === 'Component') { - components[componentName] = createComponent(component as ComponentSchema); - } else if (isReactComponent(component)) { - components[componentName] = component; - } else { - component = findComponent(libraryMap, componentName, component as NpmInfo); - if (component) { - components[componentName] = component; - } - } - }); - return components; -} - -let REACT_KEY = ''; -function cacheReactKey(el: Element): Element { - if (REACT_KEY !== '') { - return el; - } - // react17 采用 __reactFiber 开头 - REACT_KEY = Object.keys(el).find( - (key) => key.startsWith('__reactInternalInstance$') || key.startsWith('__reactFiber$'), - ) || ''; - if (!REACT_KEY && (el as HTMLElement).parentElement) { - return cacheReactKey((el as HTMLElement).parentElement!); - } - return el; -} - -function checkInstanceMounted(instance: any): boolean { - if (isElement(instance)) { - return instance.parentElement != null; - } - return true; -} - -function isValidDesignModeRaxComponentInstance( - raxComponentInst: any, -): raxComponentInst is { - props: { - _leaf: Exclude<NodeInstance<any>['node'], null | undefined>; - }; -} { - const leaf = raxComponentInst?.props?._leaf; - return leaf && typeof leaf === 'object' && leaf.isNode; -} - -export class DocumentInstance { - private instancesMap = new Map<string, any[]>(); - - private emitter = new EventEmitter(); - - get schema(): any { - return this.document.export(TransformStage.Render); - } - - constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) { - makeObservable(this); - } - - @computed get suspended(): any { - return false; - } - - @computed get scope(): any { - return null; - } - - get path(): string { - return `/${ this.document.fileName}`; - } - - get id() { - return this.document.id; - } - - private unmountIntance(id: string, instance: any) { - const instances = this.instancesMap.get(id); - if (instances) { - const i = instances.indexOf(instance); - if (i > -1) { - instances.splice(i, 1); - host.setInstance(this.document.id, id, instances); - } - } - } - - refresh() { - this.emitter.emit('rerender', { shouldRemount: true }); - } - - onReRender(fn: () => void) { - this.emitter.on('rerender', fn); - return () => { - this.emitter.removeListener('renderer', fn); - }; - } - - mountInstance(id: string, instance: any) { - const docId = this.document.id; - const { instancesMap } = this; - if (instance == null) { - let instances = this.instancesMap.get(id); - if (instances) { - instances = instances.filter(checkInstanceMounted); - if (instances.length > 0) { - instancesMap.set(id, instances); - host.setInstance(this.document.id, id, instances); - } else { - instancesMap.delete(id); - host.setInstance(this.document.id, id, null); - } - } - return; - } - const unmountIntance = this.unmountIntance.bind(this); - const origId = (instance as any)[SYMBOL_VNID]; - if (origId && origId !== id) { - // 另外一个节点的 instance 在此被复用了,需要从原来地方卸载 - unmountIntance(origId, instance); - } - if (isElement(instance)) { - cacheReactKey(instance); - } else if (origId !== id) { - // 涵盖 origId == null || origId !== id 的情况 - let origUnmount: any = instance.componentWillUnmount; - if (origUnmount && origUnmount.origUnmount) { - origUnmount = origUnmount.origUnmount; - } - // hack! delete instance from map - const newUnmount = function (this: any) { - unmountIntance(id, instance); - origUnmount && origUnmount.call(this); - }; - (newUnmount as any).origUnmount = origUnmount; - instance.componentWillUnmount = newUnmount; - } - - (instance as any)[SYMBOL_VNID] = id; - (instance as any)[SYMBOL_VDID] = docId; - let instances = this.instancesMap.get(id); - if (instances) { - const l = instances.length; - instances = instances.filter(checkInstanceMounted); - let updated = instances.length !== l; - if (!instances.includes(instance)) { - instances.push(instance); - updated = true; - } - if (!updated) { - return; - } - } else { - instances = [instance]; - } - instancesMap.set(id, instances); - host.setInstance(this.document.id, id, instances); - } - - mountContext(docId: string, id: string, ctx: object) { - // this.ctxMap.set(id, ctx); - } - - getComponentInstances(id: string): any[] | null { - return this.instancesMap.get(id) || null; - } - - getNode(id: string): Node<NodeSchema> | null { - return this.document.getNode(id); - } -} - -export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { - readonly isSimulatorRenderer = true; - private dispose?: () => void; - readonly history: MemoryHistory; - - private emitter = new EventEmitter(); - - @obx.ref private _documentInstances: DocumentInstance[] = []; - get documentInstances() { - return this._documentInstances; - } - - get currentDocumentInstance() { - return this._documentInstances.find((item) => item.id === host.project.currentDocument?.id); - } - - constructor() { - this.dispose = host.connect(this, () => { - // sync layout config - // debugger; - this._layout = host.project.get('config').layout; - // todo: split with others, not all should recompute - if (this._libraryMap !== host.libraryMap || this._componentsMap !== host.designer.componentsMap) { - this._libraryMap = host.libraryMap || {}; - this._componentsMap = host.designer.componentsMap; - this.buildComponents(); - } - - // sync designMode - this._designMode = host.designMode; - - // sync requestHandlersMap - this._requestHandlersMap = host.requestHandlersMap; - - // sync device - this._device = host.device; - - this.emitter.emit('layoutChange'); - }); - const documentInstanceMap = new Map<string, DocumentInstance>(); - let initialEntry = '/'; - let firstRun = true; - host.autorun(() => { - this._documentInstances = host.project.documents.map((doc) => { - let inst = documentInstanceMap.get(doc.id); - if (!inst) { - inst = new DocumentInstance(this, doc); - documentInstanceMap.set(doc.id, inst); - } - return inst; - }); - - const path = host.project.currentDocument ? documentInstanceMap.get(host.project.currentDocument.id)!.path : '/'; - if (firstRun) { - initialEntry = path; - firstRun = false; - } else { - if (this.history.location.pathname !== path) { - this.history.replace(path); - } - this.emitter.emit('layoutChange'); - } - }); - const history = createMemoryHistory({ - initialEntries: [initialEntry], - }); - this.history = history; - history.listen(({ location }) => { - host.project.open(location.pathname.slice(1)); - }); - host.componentsConsumer.consume(async (componentsAsset) => { - if (componentsAsset) { - await this.load(componentsAsset); - this.buildComponents(); - } - }); - this._appContext = { - utils: { - router: { - push(path: string, params?: object) { - history.push(withQueryParams(path, params)); - }, - replace(path: string, params?: object) { - history.replace(withQueryParams(path, params)); - }, - back() { - history.back(); - }, - }, - legaoBuiltins: { - getUrlParams() { - const { search } = history.location; - return parseQuery(search); - }, - }, - }, - constants: {}, - requestHandlersMap: this._requestHandlersMap, - }; - host.injectionConsumer.consume((data) => { - // sync utils, i18n, contants,... config - }); - } - - @obx private _layout: any = null; - @computed get layout(): any { - // TODO: parse layout Component - return this._layout; - } - set layout(value: any) { - this._layout = value; - } - - private _libraryMap: { [key: string]: string } = {}; - private buildComponents() { - // TODO: remove this.createComponent - this._components = buildComponents(this._libraryMap, this._componentsMap, this.createComponent.bind(this)); - } - @obx.ref private _components: any = {}; - @computed get components(): object { - // 根据 device 选择不同组件,进行响应式 - // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl - return this._components; - } - // context from: utils、constants、history、location、match - @obx.ref private _appContext = {}; - @computed get context(): any { - return this._appContext; - } - @obx.ref private _designMode: string = 'design'; - @computed get designMode(): any { - return this._designMode; - } - @obx.ref private _device: string = 'default'; - @computed get device() { - return this._device; - } - @obx.ref private _requestHandlersMap = null; - @computed get requestHandlersMap(): any { - return this._requestHandlersMap; - } - @obx.ref private _componentsMap = {}; - @computed get componentsMap(): any { - return this._componentsMap; - } - /** - * 加载资源 - */ - load(asset: Asset): Promise<any> { - return loader.load(asset); - } - - getComponent(componentName: string) { - const paths = componentName.split('.'); - const subs: string[] = []; - - while (true) { - const component = this._components[componentName]; - if (component) { - return getSubComponent(component, subs); - } - - const sub = paths.pop(); - if (!sub) { - return null; - } - subs.unshift(sub); - componentName = paths.join('.'); - } - } - - getNodeInstance(dom: HTMLElement): NodeInstance<any> | null { - const INTERNAL = '_internal'; - let instance: any = dom; - if (!isElement(instance)) { - return { - docId: instance.props._leaf.document.id, - nodeId: instance.props._leaf.getId(), - instance, - node: instance.props._leaf, - }; - } - instance = Instance.get(dom); - - let loopNum = 0; // 防止由于某种意外而导致死循环 - while (instance && instance[INTERNAL] && loopNum < 1000) { - if (isValidDesignModeRaxComponentInstance(instance)) { - // if (instance && SYMBOL_VNID in instance) { - // const docId = (instance.props as any).schema.docId; - return { - docId: instance.props._leaf.document.id, - nodeId: instance.props._leaf.getId(), - instance, - node: instance.props._leaf, - }; - } - - instance = getRaxVDomParentInstance(instance); - loopNum += 1; - } - - return null; - } - - getClosestNodeInstance(from: any, nodeId?: string): NodeInstance<any> | null { - const el: any = from; - if (el) { - // if (isElement(el)) { - // el = cacheReactKey(el); - // } else { - // return getNodeInstance(el, specId); - // } - return this.getNodeInstance(el); - } - return null; - } - - findDOMNodes(instance: any, selector?: string): Array<Element | Text> | null { - let el = instance; - if (selector) { - el = document.querySelector(selector); - } - try { - return raxFindDOMNodes(el); - } catch (e) { - // ignore - } - if (el && el.type && el.props && el.props.componentId) { - el = document.querySelector(`${el.type}[componentid=${el.props.componentId}]`); - } else { - console.error(instance); - throw new Error('This instance may not a valid element'); - } - return raxFindDOMNodes(el); - } - - getClientRects(element: Element | Text) { - return getClientRects(element); - } - - setNativeSelection(enableFlag: boolean) { - setNativeSelection(enableFlag); - } - setDraggingState(state: boolean) { - cursor.setDragging(state); - } - setCopyState(state: boolean) { - cursor.setCopy(state); - } - clearState() { - cursor.release(); - } - - onLayoutChange(cb: () => void) { - this.emitter.on('layoutChange', cb); - return () => { - this.emitter.removeListener('layoutChange', cb); - }; - } - - onReRender(fn: () => void) { - this.emitter.on('rerender', fn); - return () => { - this.emitter.removeListener('renderer', fn); - }; - } - - rerender() { - this.currentDocumentInstance?.refresh(); - } - - createComponent(schema: NodeSchema): Component | null { - const _schema: any = { - ...compatibleLegaoSchema(schema), - }; - - if (schema.componentName === 'Component' && (schema as ComponentSchema).css) { - const doc = window.document; - const s = doc.createElement('style'); - s.setAttribute('type', 'text/css'); - s.setAttribute('id', `Component-${schema.id || ''}`); - s.appendChild(doc.createTextNode((schema as ComponentSchema).css || '')); - doc.getElementsByTagName('head')[0].appendChild(s); - } - - - const renderer = this; - const { componentsMap: components } = renderer; - - class LowCodeComp extends Rax.Component { - render() { - const extraProps = getLowCodeComponentProps(this.props); - // @ts-ignore - return createElement(LowCodeRenderer, { - ...extraProps, - schema: _schema, - components, - designMode: '', - device: renderer.device, - appHelper: renderer.context, - rendererName: 'LowCodeRenderer', - thisRequiredInJSE: host.thisRequiredInJSE, - customCreateElement: (Comp: any, props: any, children: any) => { - const componentMeta = host.currentDocument?.getComponentMeta(Comp.displayName); - if (componentMeta?.isModal) { - return null; - } - - const { __id, __designMode, ...viewProps } = props; - // mock _leaf,减少性能开销 - const _leaf = { - isEmpty: () => false, - isMock: true, - }; - viewProps._leaf = _leaf; - return createElement(Comp, viewProps, children); - }, - }); - } - } - - return LowCodeComp; - } - - private _running = false; - run() { - if (this._running) { - return; - } - this._running = true; - const containerId = 'app'; - let container = document.getElementById(containerId); - if (!container) { - container = document.createElement('div'); - document.body.appendChild(container); - container.id = containerId; - } - - // ==== compatiable vision - document.documentElement.classList.add('engine-page'); - document.body.classList.add('engine-document'); // important! Stylesheet.invoke depends - - raxRender(createElement(SimulatorRendererView, { - rendererContainer: this, - }), container, { - driver: DriverUniversal, - }); - host.project.setRendererReady(this); - } -} - -function getSubComponent(library: any, paths: string[]) { - const l = paths.length; - if (l < 1 || !library) { - return library; - } - let i = 0; - let component: any; - while (i < l) { - const key = paths[i]!; - let ex: any; - try { - component = library[key]; - } catch (e) { - ex = e; - component = null; - } - if (i === 0 && component == null && key === 'default') { - if (ex) { - return l === 1 ? library : null; - } - component = library; - } else if (component == null) { - return null; - } - library = component; - i++; - } - return component; -} - -function findComponent(libraryMap: LibraryMap, componentName: string, npm?: NpmInfo) { - if (!npm) { - return accessLibrary(componentName); - } - // libraryName the key access to global - // export { exportName } from xxx exportName === global.libraryName.exportName - // export exportName from xxx exportName === global.libraryName.default || global.libraryName - // export { exportName as componentName } from package - // if exportName == null exportName === componentName; - // const componentName = exportName.subName, if exportName empty subName donot use - const exportName = npm.exportName || npm.componentName || componentName; - const libraryName = libraryMap[npm.package] || exportName; - const library = accessLibrary(libraryName); - const paths = npm.exportName && npm.subName ? npm.subName.split('.') : []; - if (npm.destructuring) { - paths.unshift(exportName); - } else if (isESModule(library)) { - paths.unshift('default'); - } - return getSubComponent(library, paths); -} - -function getLowCodeComponentProps(props: any) { - if (!props || !isPlainObject(props)) { - return props; - } - const newProps: any = {}; - Object.keys(props).forEach(k => { - if (['children', 'componentId', '__designMode', '_componentName', '_leaf'].includes(k)) { - return; - } - newProps[k] = props[k]; - }); - return newProps; -} - -/** - * 获取 Rax 里面 VDOM 的上一级的实例 - * 注意:Rax 的 development 的包是带有 __parentInstance, - * 但是 production 的包 __parentInstance 会被压缩掉, - * 所以这里遍历下其中的所有值,尝试找到有 _internal 的那个(别的值不会带有这个属性的) - */ -function getRaxVDomParentInstance(instance: { _internal: any }) { - const internalInstance = instance._internal; - return internalInstance.__parentInstance || - Object.values(internalInstance).find(v => ( - v !== null && - v !== instance && - typeof v === 'object' && - typeof (v as {_internal: unknown})._internal === 'object' - )); -} - -export default new SimulatorRendererContainer(); diff --git a/packages/rax-simulator-renderer/src/utils/create-defer.ts b/packages/rax-simulator-renderer/src/utils/create-defer.ts deleted file mode 100644 index e7997365a0..0000000000 --- a/packages/rax-simulator-renderer/src/utils/create-defer.ts +++ /dev/null @@ -1,17 +0,0 @@ -export interface Defer<T = any> { - resolve(value?: T | PromiseLike<T>): void; - reject(reason?: any): void; - promise(): Promise<T>; -} - -export function createDefer<T = any>(): Defer<T> { - const r: any = {}; - const promise = new Promise<T>((resolve, reject) => { - r.resolve = resolve; - r.reject = reject; - }); - - r.promise = () => promise; - - return r; -} diff --git a/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts b/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts deleted file mode 100644 index 106af2efac..0000000000 --- a/packages/rax-simulator-renderer/src/utils/find-dom-nodes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isElement } from '@alilc/lowcode-utils'; -import findDOMNode from 'rax-find-dom-node'; -// import { isDOMNode } from './is-dom-node'; - -export function raxFindDOMNodes(instance: any): Array<Element | Text> | null { - if (!instance) { - return null; - } - if (isElement(instance)) { - return [instance]; - } - // eslint-disable-next-line react/no-find-dom-node - const result = findDOMNode(instance); - if (Array.isArray(result)) { - return result; - } - return [result]; -} diff --git a/packages/rax-simulator-renderer/src/utils/get-client-rects.ts b/packages/rax-simulator-renderer/src/utils/get-client-rects.ts deleted file mode 100644 index dd13aba81e..0000000000 --- a/packages/rax-simulator-renderer/src/utils/get-client-rects.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { isElement } from '@alilc/lowcode-utils'; - -// a range for test TextNode clientRect -const cycleRange = document.createRange(); - -export function getClientRects(node: Element | Text) { - if (isElement(node)) { - return [node.getBoundingClientRect()]; - } - - cycleRange.selectNode(node); - return Array.from(cycleRange.getClientRects()); -} diff --git a/packages/rax-simulator-renderer/src/utils/get-device-view.ts b/packages/rax-simulator-renderer/src/utils/get-device-view.ts deleted file mode 100644 index 9005562599..0000000000 --- a/packages/rax-simulator-renderer/src/utils/get-device-view.ts +++ /dev/null @@ -1,23 +0,0 @@ -function ucfirst(s: string) { - return s.charAt(0).toUpperCase() + s.substring(1); -} -function getDeviceView(view: any, device: string, mode: string) { - if (!view || typeof view === 'string') { - return view; - } - - // compatible vision Mobile | Preview - device = ucfirst(device); - if (device === 'Mobile' && view.hasOwnProperty(device)) { - view = view[device]; - } - mode = ucfirst(mode); - if (mode === 'Preview' && view.hasOwnProperty(mode)) { - view = view[mode]; - } - return view; -} - -export default { - getDeviceView, -}; diff --git a/packages/rax-simulator-renderer/src/utils/is-dom-node.ts b/packages/rax-simulator-renderer/src/utils/is-dom-node.ts deleted file mode 100644 index bfbeb79c1f..0000000000 --- a/packages/rax-simulator-renderer/src/utils/is-dom-node.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function isDOMNode(node: any): node is Element | Text { - if (!node) return false; - return node.nodeType && (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE); -} diff --git a/packages/rax-simulator-renderer/src/utils/loader.ts b/packages/rax-simulator-renderer/src/utils/loader.ts deleted file mode 100644 index 436e51c441..0000000000 --- a/packages/rax-simulator-renderer/src/utils/loader.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { load, evaluate } from './script'; -import StylePoint from './style'; -import { - Asset, - AssetLevel, - AssetLevels, - AssetType, - AssetList, - isAssetBundle, - isAssetItem, - assetItem, - AssetItem, - isCSSUrl, -} from '@alilc/lowcode-utils'; - -function parseAssetList(scripts: any, styles: any, assets: AssetList, level?: AssetLevel) { - for (const asset of assets) { - parseAsset(scripts, styles, asset, level); - } -} - -function parseAsset(scripts: any, styles: any, asset: Asset | undefined | null, level?: AssetLevel) { - if (!asset) { - return; - } - if (Array.isArray(asset)) { - return parseAssetList(scripts, styles, asset, level); - } - - if (isAssetBundle(asset)) { - if (asset.assets) { - if (Array.isArray(asset.assets)) { - parseAssetList(scripts, styles, asset.assets, asset.level || level); - } else { - parseAsset(scripts, styles, asset.assets, asset.level || level); - } - return; - } - return; - } - - if (!isAssetItem(asset)) { - asset = assetItem(isCSSUrl(asset) ? AssetType.CSSUrl : AssetType.JSUrl, asset, level)!; - } - - let lv = asset.level || level; - - if (!lv || AssetLevel[lv] == null) { - lv = AssetLevel.App; - } - - asset.level = lv; - if (asset.type === AssetType.CSSUrl || asset.type == AssetType.CSSText) { - styles[lv].push(asset); - } else { - scripts[lv].push(asset); - } -} - -export class AssetLoader { - async load(asset: Asset) { - const styles: any = {}; - const scripts: any = {}; - AssetLevels.forEach(lv => { - styles[lv] = []; - scripts[lv] = []; - }); - parseAsset(scripts, styles, asset); - const styleQueue: AssetItem[] = styles[AssetLevel.Environment].concat( - styles[AssetLevel.Library], - styles[AssetLevel.Theme], - styles[AssetLevel.Runtime], - styles[AssetLevel.App], - ); - const scriptQueue: AssetItem[] = scripts[AssetLevel.Environment].concat( - scripts[AssetLevel.Library], - scripts[AssetLevel.Theme], - scripts[AssetLevel.Runtime], - scripts[AssetLevel.App], - ); - await Promise.all( - styleQueue.map(({ content, level, type, id }) => this.loadStyle(content, level!, type === AssetType.CSSUrl, id)), - ); - await Promise.all(scriptQueue.map(({ content, type }) => this.loadScript(content, type === AssetType.JSUrl))); - } - - private stylePoints = new Map<string, StylePoint>(); - - private loadStyle(content: string | undefined | null, level: AssetLevel, isUrl?: boolean, id?: string) { - if (!content) { - return; - } - let point: StylePoint | undefined; - if (id) { - point = this.stylePoints.get(id); - if (!point) { - point = new StylePoint(level, id); - this.stylePoints.set(id, point); - } - } else { - point = new StylePoint(level); - } - return isUrl ? point.applyUrl(content) : point.applyText(content); - } - - private loadScript(content: string | undefined | null, isUrl?: boolean) { - if (!content) { - return; - } - return isUrl ? load(content) : evaluate(content); - } -} - -export default new AssetLoader(); diff --git a/packages/rax-simulator-renderer/src/utils/script.ts b/packages/rax-simulator-renderer/src/utils/script.ts deleted file mode 100644 index 81841ff6d2..0000000000 --- a/packages/rax-simulator-renderer/src/utils/script.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { createDefer } from './create-defer'; - -export function evaluate(script: string) { - const scriptEl = document.createElement('script'); - scriptEl.text = script; - document.head.appendChild(scriptEl); - document.head.removeChild(scriptEl); -} - -export function load(url: string) { - const node: any = document.createElement('script'); - - // node.setAttribute('crossorigin', 'anonymous'); - - node.onload = onload; - node.onerror = onload; - - const i = createDefer(); - - function onload(e: any) { - node.onload = null; - node.onerror = null; - if (e.type === 'load') { - i.resolve(); - } else { - i.reject(); - } - // document.head.removeChild(node); - // node = null; - } - - // node.async = true; - node.src = url; - - document.head.appendChild(node); - - return i.promise(); -} - -export function evaluateExpression(expr: string) { - // eslint-disable-next-line no-new-func - const fn = new Function(expr); - return fn(); -} - -export function newFunction(args: string, code: string) { - try { - // eslint-disable-next-line no-new-func - return new Function(args, code); - } catch (e) { - console.warn('Caught error, Cant init func'); - return null; - } -} diff --git a/packages/rax-simulator-renderer/src/utils/style.ts b/packages/rax-simulator-renderer/src/utils/style.ts deleted file mode 100644 index 91dbbc6345..0000000000 --- a/packages/rax-simulator-renderer/src/utils/style.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createDefer } from './create-defer'; - -export default class StylePoint { - private lastContent: string | undefined; - - private lastUrl: string | undefined; - - private placeholder: Element | Text; - - constructor(readonly level: number, readonly id?: string) { - let placeholder: any; - if (id) { - placeholder = document.head.querySelector(`style[data-id="${id}"]`); - } - if (!placeholder) { - placeholder = document.createTextNode(''); - const meta = document.head.querySelector(`meta[level="${level}"]`); - if (meta) { - document.head.insertBefore(placeholder, meta); - } else { - document.head.appendChild(placeholder); - } - } - this.placeholder = placeholder; - } - - applyText(content: string) { - if (this.lastContent === content) { - return; - } - this.lastContent = content; - this.lastUrl = undefined; - const element = document.createElement('style'); - element.setAttribute('type', 'text/css'); - if (this.id) { - element.setAttribute('data-id', this.id); - } - element.appendChild(document.createTextNode(content)); - document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); - document.head.removeChild(this.placeholder); - this.placeholder = element; - } - - applyUrl(url: string) { - if (this.lastUrl === url) { - return; - } - this.lastContent = undefined; - this.lastUrl = url; - const element = document.createElement('link'); - element.onload = onload; - element.onerror = onload; - - const i = createDefer(); - function onload(e: any) { - element.onload = null; - element.onerror = null; - if (e.type === 'load') { - i.resolve(); - } else { - i.reject(); - } - } - - element.href = url; - element.rel = 'stylesheet'; - if (this.id) { - element.setAttribute('data-id', this.id); - } - document.head.insertBefore(element, this.placeholder.parentNode === document.head ? this.placeholder.nextSibling : null); - document.head.removeChild(this.placeholder); - this.placeholder = element; - return i.promise(); - } -} diff --git a/packages/rax-simulator-renderer/src/utils/url.ts b/packages/rax-simulator-renderer/src/utils/url.ts deleted file mode 100644 index d720323b3a..0000000000 --- a/packages/rax-simulator-renderer/src/utils/url.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * Parse queryString - * @param {String} str '?q=query&b=test' - * @return {Object} - */ -export function parseQuery(str: string): object { - const ret: any = {}; - - if (typeof str !== 'string') { - return ret; - } - - const s = str.trim().replace(/^(\?|#|&)/, ''); - - if (!s) { - return ret; - } - - s.split('&').forEach((param) => { - const parts = param.replace(/\+/g, ' ').split('='); - let key = parts.shift()!; - let val: any = parts.length > 0 ? parts.join('=') : undefined; - - key = decodeURIComponent(key); - - val = val === undefined ? null : decodeURIComponent(val); - - if (ret[key] === undefined) { - ret[key] = val; - } else if (Array.isArray(ret[key])) { - ret[key].push(val); - } else { - ret[key] = [ret[key], val]; - } - }); - - return ret; -} - -/** - * Stringify object to query parammeters - * @param {Object} obj - * @return {String} - */ -export function stringifyQuery(obj: any): string { - const param: string[] = []; - Object.keys(obj).forEach((key) => { - let value = obj[key]; - if (value && typeof value === 'object') { - value = JSON.stringify(value); - } - param.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`); - }); - return param.join('&'); -} - -export function uriEncode(uri: string) { - return encodeURIComponent(uri); -} - -export function uriDecode(uri: string) { - return decodeURIComponent(uri); -} - -export function withQueryParams(url: string, params?: object) { - const queryStr = params ? stringifyQuery(params) : ''; - if (queryStr === '') { - return url; - } - const urlSplit = url.split('#'); - const hash = urlSplit[1] ? `#${urlSplit[1]}` : ''; - const urlWithoutHash = urlSplit[0]; - return `${urlWithoutHash}${~urlWithoutHash.indexOf('?') ? '&' : '?'}${queryStr}${hash}`; -} diff --git a/packages/react-renderer/build.json b/packages/react-renderer/build.json index e791d5b6b3..d0aec10385 100644 --- a/packages/react-renderer/build.json +++ b/packages/react-renderer/build.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "build-plugin-fusion", ["build-plugin-moment-locales", { "locales": ["zh-cn"] diff --git a/packages/react-renderer/build.test.json b/packages/react-renderer/build.test.json index dcdc891e93..9cc30d7463 100644 --- a/packages/react-renderer/build.test.json +++ b/packages/react-renderer/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ] } diff --git a/packages/react-renderer/jest.config.js b/packages/react-renderer/jest.config.js index 74fdef1758..df1400719b 100644 --- a/packages/react-renderer/jest.config.js +++ b/packages/react-renderer/jest.config.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const esModules = ['zen-logger'].join('|'); +const esModules = [].join('|'); const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); const jestConfig = { diff --git a/packages/react-renderer/package.json b/packages/react-renderer/package.json index 62e782c4b4..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.0.15", + "version": "1.3.2", "description": "react renderer for ali lowcode engine", "main": "lib/index.js", "module": "es/index.js", @@ -12,7 +12,7 @@ "scripts": { "test": "build-scripts test --config build.test.json", "start": "build-scripts start", - "build": "build-scripts build --skip-demo", + "build": "build-scripts build", "build:umd": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --config build.umd.json" }, "keywords": [ @@ -22,12 +22,11 @@ ], "dependencies": { "@alifd/next": "^1.21.16", - "@alilc/lowcode-renderer-core": "1.0.15" + "@alilc/lowcode-renderer-core": "1.3.2" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", "@alifd/next": "^1.19.17", - "build-plugin-component": "^0.2.10", "build-plugin-fusion": "^0.1.0", "build-plugin-moment-locales": "^0.1.0", "react": "^16.4.1", @@ -42,6 +41,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/react-renderer" }, - "homepage": "https://unpkg.alibaba-inc.com/@alilc/lowcode-react-renderer@1.0.21/build/index.html", - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "homepage": "https://github.com/alibaba/lowcode-engine/#readme", + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues" } diff --git a/packages/react-simulator-renderer/babel.config.js b/packages/react-simulator-renderer/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/packages/react-simulator-renderer/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/react-simulator-renderer/build.json b/packages/react-simulator-renderer/build.json index b95a17aafe..e7ae1dcf7a 100644 --- a/packages/react-simulator-renderer/build.json +++ b/packages/react-simulator-renderer/build.json @@ -1,3 +1,3 @@ { - "plugins": ["build-plugin-component", "./build.plugin.js"] + "plugins": ["@alilc/build-plugin-lce", "./build.plugin.js"] } diff --git a/packages/react-simulator-renderer/build.test.json b/packages/react-simulator-renderer/build.test.json index dcdc891e93..9cc30d7463 100644 --- a/packages/react-simulator-renderer/build.test.json +++ b/packages/react-simulator-renderer/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ] } diff --git a/packages/react-simulator-renderer/jest.config.js b/packages/react-simulator-renderer/jest.config.js index 28b46c249c..5378ef5380 100644 --- a/packages/react-simulator-renderer/jest.config.js +++ b/packages/react-simulator-renderer/jest.config.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const esModules = ['zen-logger'].join('|'); +const esModules = [].join('|'); const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); const jestConfig = { diff --git a/packages/react-simulator-renderer/package.json b/packages/react-simulator-renderer/package.json index 3846167ba2..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.0.15", + "version": "1.3.2", "description": "react simulator renderer for alibaba lowcode designer", "main": "lib/index.js", "module": "es/index.js", @@ -12,15 +12,15 @@ ], "scripts": { "test": "build-scripts test --config build.test.json", - "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --skip-demo", + "build": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build", "build:umd": "NODE_OPTIONS=--max_old_space_size=8192 build-scripts build --config build.umd.json", "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "dependencies": { - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-react-renderer": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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", @@ -33,8 +33,7 @@ "@types/node": "^13.7.1", "@types/react": "^16", "@types/react-dom": "^16", - "@types/react-router": "^5.1.17", - "build-plugin-component": "^0.2.11" + "@types/react-router": "5.1.18" }, "publishConfig": { "access": "public", @@ -44,5 +43,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/react-simulator-renderer" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/react-simulator-renderer/src/locale/en-US.json b/packages/react-simulator-renderer/src/locale/en-US.json new file mode 100644 index 0000000000..ac864c0a29 --- /dev/null +++ b/packages/react-simulator-renderer/src/locale/en-US.json @@ -0,0 +1,4 @@ +{ + "Drag and drop components or templates here": "Drag and drop components or templates here", + "Locked elements and child elements cannot be edited": "Locked elements and child elements cannot be edited" +} \ No newline at end of file diff --git a/packages/react-simulator-renderer/src/locale/index.ts b/packages/react-simulator-renderer/src/locale/index.ts new file mode 100644 index 0000000000..5f4ef01505 --- /dev/null +++ b/packages/react-simulator-renderer/src/locale/index.ts @@ -0,0 +1,21 @@ +import { createElement } from 'react'; +import enUS from './en-US.json'; +import zhCN from './zh-CN.json'; + +const instance: Record<string, Record<string, string>> = { + 'zh-CN': zhCN as Record<string, string>, + 'en-US': enUS as Record<string, string>, +}; + +export function createIntl(locale: string = 'zh-CN') { + const intl = (id: string) => { + return instance[locale]?.[id] || id; + }; + + const intlNode = (id: string) => createElement('span', instance[locale]?.[id] || id); + + return { + intl, + intlNode, + }; +} diff --git a/packages/react-simulator-renderer/src/locale/zh-CN.json b/packages/react-simulator-renderer/src/locale/zh-CN.json new file mode 100644 index 0000000000..74bb821dd2 --- /dev/null +++ b/packages/react-simulator-renderer/src/locale/zh-CN.json @@ -0,0 +1,4 @@ +{ + "Drag and drop components or templates here": "拖拽组件或模板到这里", + "Locked elements and child elements cannot be edited": "锁定元素及子元素无法编辑" +} \ No newline at end of file diff --git a/packages/react-simulator-renderer/src/renderer-view.tsx b/packages/react-simulator-renderer/src/renderer-view.tsx index e68d9c401b..aa1683cd22 100644 --- a/packages/react-simulator-renderer/src/renderer-view.tsx +++ b/packages/react-simulator-renderer/src/renderer-view.tsx @@ -10,6 +10,7 @@ import { SimulatorRendererContainer, DocumentInstance } from './renderer'; import { host } from './host'; import { isRendererDetached } from './utils/misc'; import './renderer.less'; +import { createIntl } from './locale'; // patch cloneElement avoid lost keyProps const originCloneElement = window.React.cloneElement; @@ -130,6 +131,7 @@ class Renderer extends Component<{ documentInstance: DocumentInstance; }> { startTime: number | null = null; + schemaChangedSymbol = false; componentDidUpdate() { this.recordTime(); @@ -139,7 +141,7 @@ class Renderer extends Component<{ if (this.startTime) { const time = Date.now() - this.startTime; const nodeCount = host.designer.currentDocument?.getNodeCount?.(); - host.designer.editor?.emit(GlobalEvent.Node.Rerender, { + host.designer.editor?.eventBus.emit(GlobalEvent.Node.Rerender, { componentName: 'Renderer', type: 'All', time, @@ -152,8 +154,6 @@ class Renderer extends Component<{ this.recordTime(); } - schemaChangedSymbol = false; - getSchemaChangedSymbol = () => { return this.schemaChangedSymbol; }; @@ -170,7 +170,12 @@ class Renderer extends Component<{ this.startTime = Date.now(); this.schemaChangedSymbol = false; - if (!container.autoRender || isRendererDetached()) return null; + if (!container.autoRender || isRendererDetached()) { + return null; + } + + const { intl } = createIntl(locale); + return ( <LowCodeRenderer locale={locale} @@ -188,6 +193,9 @@ class Renderer extends Component<{ getNode={(id: string) => documentInstance.getNode(id) as Node} rendererName="PageRenderer" thisRequiredInJSE={host.thisRequiredInJSE} + notFoundComponent={host.notFoundComponent} + faultComponent={host.faultComponent} + faultComponentMap={host.faultComponentMap} customCreateElement={(Component: any, props: any, children: any) => { const { __id, ...viewProps } = props; viewProps.componentId = __id; @@ -203,12 +211,12 @@ class Renderer extends Component<{ (children == null || (Array.isArray(children) && !children.length)) && (!viewProps.style || Object.keys(viewProps.style).length === 0) ) { - let defaultPlaceholder = '拖拽组件或模板到这里'; + let defaultPlaceholder = intl('Drag and drop components or templates here'); const lockedNode = getClosestNode(leaf, (node) => { return node?.getExtraProp('isLocked')?.getValue() === true; }); if (lockedNode) { - defaultPlaceholder = '锁定元素及子元素无法编辑'; + defaultPlaceholder = intl('Locked elements and child elements cannot be edited'); } children = ( <div className={cn('lc-container-placeholder', { 'lc-container-locked': !!lockedNode })} style={viewProps.placeholderStyle}> diff --git a/packages/react-simulator-renderer/src/renderer.ts b/packages/react-simulator-renderer/src/renderer.ts index 2dfdeccac6..20f6e18c0b 100644 --- a/packages/react-simulator-renderer/src/renderer.ts +++ b/packages/react-simulator-renderer/src/renderer.ts @@ -4,7 +4,7 @@ import { host } from './host'; import SimulatorRendererView from './renderer-view'; import { computed, observable as obx, untracked, makeObservable, configure } from 'mobx'; import { getClientRects } from './utils/get-client-rects'; -import { reactFindDOMNodes, FIBER_KEY } from './utils/react-find-dom-nodes'; +import { reactFindDOMNodes, getReactInternalFiber } from './utils/react-find-dom-nodes'; import { Asset, isElement, @@ -17,9 +17,9 @@ import { AssetLoader, getProjectUtils, } from '@alilc/lowcode-utils'; -import { ComponentSchema, TransformStage, NodeSchema } from '@alilc/lowcode-types'; +import { IPublicTypeComponentSchema, IPublicEnumTransformStage, IPublicTypeNodeInstance, IPublicTypeProjectSchema } from '@alilc/lowcode-types'; // just use types -import { BuiltinSimulatorRenderer, NodeInstance, Component, DocumentModel, Node } from '@alilc/lowcode-designer'; +import { BuiltinSimulatorRenderer, Component, IDocumentModel, INode } from '@alilc/lowcode-designer'; import LowCodeRenderer from '@alilc/lowcode-react-renderer'; import { createMemoryHistory, MemoryHistory } from 'history'; import Slot from './builtin-components/slot'; @@ -31,18 +31,14 @@ const loader = new AssetLoader(); configure({ enforceActions: 'never' }); export class DocumentInstance { - public instancesMap = new Map<string, ReactInstance[]>(); + instancesMap = new Map<string, ReactInstance[]>(); get schema(): any { - return this.document.export(TransformStage.Render); + return this.document.export(IPublicEnumTransformStage.Render); } private disposeFunctions: Array<() => void> = []; - constructor(readonly container: SimulatorRendererContainer, readonly document: DocumentModel) { - makeObservable(this); - } - @obx.ref private _components: any = {}; @computed get components(): object { @@ -98,6 +94,10 @@ export class DocumentInstance { return this.document.id; } + constructor(readonly container: SimulatorRendererContainer, readonly document: IDocumentModel) { + makeObservable(this); + } + private unmountInstance(id: string, instance: ReactInstance) { const instances = this.instancesMap.get(id); if (instances) { @@ -170,11 +170,10 @@ export class DocumentInstance { host.setInstance(this.document.id, id, instances); } - mountContext(docId: string, id: string, ctx: object) { - // this.ctxMap.set(id, ctx); + mountContext() { } - getNode(id: string): Node | null { + getNode(id: string): INode | null { return this.document.getNode(id); } @@ -190,10 +189,65 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { readonly history: MemoryHistory; @obx.ref private _documentInstances: DocumentInstance[] = []; + private _requestHandlersMap: any; get documentInstances() { return this._documentInstances; } + @obx private _layout: any = null; + + @computed get layout(): any { + // TODO: parse layout Component + return this._layout; + } + + set layout(value: any) { + this._layout = value; + } + + private _libraryMap: { [key: string]: string } = {}; + + private _components: Record<string, React.FC | React.ComponentClass> | null = {}; + + get components(): Record<string, React.FC | React.ComponentClass> { + // 根据 device 选择不同组件,进行响应式 + // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl + return this._components || {}; + } + // context from: utils、constants、history、location、match + @obx.ref private _appContext: any = {}; + @computed get context(): any { + return this._appContext; + } + @obx.ref private _designMode: string = 'design'; + @computed get designMode(): any { + return this._designMode; + } + @obx.ref private _device: string = 'default'; + @computed get device() { + return this._device; + } + @obx.ref private _locale: string | undefined = undefined; + @computed get locale() { + return this._locale; + } + @obx.ref private _componentsMap = {}; + @computed get componentsMap(): any { + return this._componentsMap; + } + + /** + * 是否为画布自动渲染 + */ + autoRender = true; + + /** + * 画布是否自动监听事件来重绘节点 + */ + autoRepaintNode = true; + + private _running = false; + constructor() { makeObservable(this); this.autoRender = host.autoRender; @@ -203,7 +257,8 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { this._layout = host.project.get('config').layout; // todo: split with others, not all should recompute - if (this._libraryMap !== host.libraryMap || this._componentsMap !== host.designer.componentsMap) { + if (this._libraryMap !== host.libraryMap + || this._componentsMap !== host.designer.componentsMap) { this._libraryMap = host.libraryMap || {}; this._componentsMap = host.designer.componentsMap; this.buildComponents(); @@ -246,7 +301,7 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { initialEntries: [initialEntry], }); this.history = history; - history.listen((location, action) => { + history.listen((location) => { const docId = location.pathname.slice(1); docId && host.project.open(docId); }); @@ -285,70 +340,37 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { constants: {}, requestHandlersMap: this._requestHandlersMap, }; + host.injectionConsumer.consume((data) => { // TODO: sync utils, i18n, contants,... config const newCtx = { ...this._appContext, }; - newCtx.utils.i18n.messages = data.i18n || {}; merge(newCtx, data.appHelper || {}); this._appContext = newCtx; }); - } - - @obx private _layout: any = null; - - @computed get layout(): any { - // TODO: parse layout Component - return this._layout; - } - set layout(value: any) { - this._layout = value; + host.i18nConsumer.consume((data) => { + const newCtx = { + ...this._appContext, + }; + newCtx.utils.i18n.messages = data || {}; + this._appContext = newCtx; + }); } - private _libraryMap: { [key: string]: string } = {}; - private buildComponents() { - this._components = buildComponents(this._libraryMap, this._componentsMap, this.createComponent.bind(this)); + this._components = buildComponents( + this._libraryMap, + this._componentsMap, + this.createComponent.bind(this), + ); this._components = { ...builtinComponents, ...this._components, }; } - private _components: any = {}; - - get components(): object { - // 根据 device 选择不同组件,进行响应式 - // 更好的做法是,根据 device 选择加载不同的组件资源,甚至是 simulatorUrl - return this._components; - } - // context from: utils、constants、history、location、match - @obx.ref private _appContext: any = {}; - @computed get context(): any { - return this._appContext; - } - @obx.ref private _designMode: string = 'design'; - @computed get designMode(): any { - return this._designMode; - } - @obx.ref private _device: string = 'default'; - @computed get device() { - return this._device; - } - @obx.ref private _locale: string | undefined = undefined; - @computed get locale() { - return this._locale; - } - @obx.ref private _componentsMap = {}; - @computed get componentsMap(): any { - return this._componentsMap; - } - /** - * 是否为画布自动渲染 - */ - autoRender = true; /** * 加载资源 */ @@ -366,7 +388,7 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { const subs: string[] = []; while (true) { - const component = this._components[componentName]; + const component = this._components?.[componentName]; if (component) { return getSubComponent(component, subs); } @@ -380,7 +402,7 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { } } - getClosestNodeInstance(from: ReactInstance, nodeId?: string): NodeInstance<ReactInstance> | null { + getClosestNodeInstance(from: ReactInstance, nodeId?: string): IPublicTypeNodeInstance<ReactInstance> | null { return getClosestNodeInstance(from, nodeId); } @@ -408,17 +430,20 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { cursor.release(); } - createComponent(schema: NodeSchema): Component | null { - const _schema: any = { - ...compatibleLegaoSchema(schema), + createComponent(schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema>): Component | null { + const _schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema> = { + ...schema, + componentsTree: schema.componentsTree.map(compatibleLegaoSchema), }; - if (schema.componentName === 'Component' && (schema as ComponentSchema).css) { + const componentsTreeSchema = _schema.componentsTree[0]; + + if (componentsTreeSchema.componentName === 'Component' && componentsTreeSchema.css) { const doc = window.document; const s = doc.createElement('style'); s.setAttribute('type', 'text/css'); - s.setAttribute('id', `Component-${schema.id || ''}`); - s.appendChild(doc.createTextNode((schema as ComponentSchema).css || '')); + s.setAttribute('id', `Component-${componentsTreeSchema.id || ''}`); + s.appendChild(doc.createTextNode(componentsTreeSchema.css || '')); doc.getElementsByTagName('head')[0].appendChild(s); } @@ -430,13 +455,17 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { return createElement(LowCodeRenderer, { ...extraProps, // 防止覆盖下面内置属性 // 使用 _schema 为了使低代码组件在页面设计中使用变量,同 react 组件使用效果一致 - schema: _schema, + schema: componentsTreeSchema, components: renderer.components, designMode: '', + locale: renderer.locale, + messages: _schema.i18n || {}, device: renderer.device, appHelper: renderer.context, rendererName: 'LowCodeRenderer', thisRequiredInJSE: host.thisRequiredInJSE, + faultComponent: host.faultComponent, + faultComponentMap: host.faultComponentMap, customCreateElement: (Comp: any, props: any, children: any) => { const componentMeta = host.currentDocument?.getComponentMeta(Comp.displayName); if (componentMeta?.isModal) { @@ -459,8 +488,6 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { return LowCodeComp; } - private _running = false; - run() { if (this._running) { return; @@ -491,9 +518,17 @@ export class SimulatorRendererContainer implements BuiltinSimulatorRenderer { this._appContext = { ...this._appContext }; } + stopAutoRepaintNode() { + this.autoRepaintNode = false; + } + + enableAutoRepaintNode() { + this.autoRepaintNode = true; + } + dispose() { - this.disposeFunctions.forEach(fn => fn()); - this.documentInstances.forEach(docInst => docInst.dispose()); + this.disposeFunctions.forEach((fn) => fn()); + this.documentInstances.forEach((docInst) => docInst.dispose()); untracked(() => { this._componentsMap = {}; this._components = null; @@ -527,13 +562,16 @@ function cacheReactKey(el: Element): Element { const SYMBOL_VNID = Symbol('_LCNodeId'); const SYMBOL_VDID = Symbol('_LCDocId'); -function getClosestNodeInstance(from: ReactInstance, specId?: string): NodeInstance<ReactInstance> | null { +function getClosestNodeInstance( + from: ReactInstance, + specId?: string, + ): IPublicTypeNodeInstance<ReactInstance> | null { let el: any = from; if (el) { if (isElement(el)) { el = cacheReactKey(el); } else { - return getNodeInstance(el[FIBER_KEY], specId); + return getNodeInstance(getReactInternalFiber(el), specId); } } while (el) { @@ -557,7 +595,7 @@ function getClosestNodeInstance(from: ReactInstance, specId?: string): NodeInsta return null; } -function getNodeInstance(fiberNode: any, specId?: string): NodeInstance<ReactInstance> | null { +function getNodeInstance(fiberNode: any, specId?: string): IPublicTypeNodeInstance<ReactInstance> | null { const instance = fiberNode?.stateNode; if (instance && SYMBOL_VNID in instance) { const nodeId = instance[SYMBOL_VNID]; @@ -576,7 +614,7 @@ function getNodeInstance(fiberNode: any, specId?: string): NodeInstance<ReactIns function checkInstanceMounted(instance: any): boolean { if (isElement(instance)) { - return instance.parentElement != null; + return instance.parentElement != null && window.document.contains(instance); } return true; } @@ -586,12 +624,13 @@ function getLowCodeComponentProps(props: any) { return props; } const newProps: any = {}; - Object.keys(props).forEach(k => { + Object.keys(props).forEach((k) => { if (['children', 'componentId', '__designMode', '_componentName', '_leaf'].includes(k)) { return; } newProps[k] = props[k]; }); + newProps['componentName'] = props['_componentName']; return newProps; } diff --git a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts index eb1fb41d50..d7af90346f 100644 --- a/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts +++ b/packages/react-simulator-renderer/src/utils/react-find-dom-nodes.ts @@ -3,7 +3,9 @@ import { findDOMNode } from 'react-dom'; import { isElement } from '@alilc/lowcode-utils'; import { isDOMNode } from './is-dom-node'; -export const FIBER_KEY = '_reactInternalFiber'; +export const getReactInternalFiber = (el: any) => { + return el._reactInternals || el._reactInternalFiber; +}; function elementsFromFiber(fiber: any, elements: Array<Element | Text>) { if (fiber) { @@ -28,7 +30,7 @@ export function reactFindDOMNodes(elem: ReactInstance | null): Array<Element | T return [elem]; } const elements: Array<Element | Text> = []; - const fiberNode = (elem as any)[FIBER_KEY]; + const fiberNode = getReactInternalFiber(elem); elementsFromFiber(fiberNode?.child, elements); if (elements.length > 0) return elements; try { diff --git a/packages/react-simulator-renderer/test/schema/basic.ts b/packages/react-simulator-renderer/test/schema/basic.ts index c32d1d864b..5dffd7267f 100644 --- a/packages/react-simulator-renderer/test/schema/basic.ts +++ b/packages/react-simulator-renderer/test/schema/basic.ts @@ -34,11 +34,11 @@ export default { id: 'node_ockvuu8u916', props: { content: { - use: 'zh_CN', + use: 'zh-CN', type: 'JSExpression', - en_US: 'Tips content', + 'en-US': 'Tips content', value: '"我是一个简单的测试页面"', - zh_CN: '我是一个简单的测试页面', + 'zh-CN': '我是一个简单的测试页面', extType: 'i18n', }, fieldId: 'text_kvuu9gl2', diff --git a/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap b/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap index 0ebb606dac..2f2d19f269 100644 --- a/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap +++ b/packages/react-simulator-renderer/test/src/renderer/__snapshots__/demo.test.tsx.snap @@ -31,6 +31,7 @@ exports[`Base should be render Text 1`] = ` behavior="NORMAL" componentId="node_ockvuu8u916" fieldId="text_kvuu9gl2" + forwardRef={[Function]} maxLine={0} showTitle={false} > diff --git a/packages/react-simulator-renderer/test/utils/host.ts b/packages/react-simulator-renderer/test/utils/host.ts index f5d54d112b..f7ab343579 100644 --- a/packages/react-simulator-renderer/test/utils/host.ts +++ b/packages/react-simulator-renderer/test/utils/host.ts @@ -62,6 +62,10 @@ class Host { consume() {} } + i18nConsumer = { + consume() {} + } + /** 下列的函数或者方法是方便测试用 */ mockSchema = (schema: any) => { this.schema = schema; diff --git a/packages/renderer-core/babel.config.js b/packages/renderer-core/babel.config.js new file mode 100644 index 0000000000..c5986f2bc0 --- /dev/null +++ b/packages/renderer-core/babel.config.js @@ -0,0 +1 @@ +module.exports = require('../../babel.config'); \ No newline at end of file diff --git a/packages/renderer-core/build.json b/packages/renderer-core/build.json index a8e42f1540..9140815c5e 100644 --- a/packages/renderer-core/build.json +++ b/packages/renderer-core/build.json @@ -1,7 +1,7 @@ { "plugins": [ [ - "build-plugin-component", + "@alilc/build-plugin-lce", { "babelPlugins": ["@babel/plugin-transform-typescript"] } diff --git a/packages/renderer-core/build.test.json b/packages/renderer-core/build.test.json index dcdc891e93..9cc30d7463 100644 --- a/packages/renderer-core/build.test.json +++ b/packages/renderer-core/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ] } diff --git a/packages/renderer-core/jest.config.js b/packages/renderer-core/jest.config.js index ceb8e8b563..1ea4204de5 100644 --- a/packages/renderer-core/jest.config.js +++ b/packages/renderer-core/jest.config.js @@ -1,6 +1,6 @@ const fs = require('fs'); const { join } = require('path'); -const esModules = ['zen-logger'].join('|'); +const esModules = [].join('|'); const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); const jestConfig = { @@ -11,6 +11,9 @@ const jestConfig = { // }, // testMatch: ['(/tests?/.*(test))\\.[jt]s$'], // testMatch: ['**/*/base.test.tsx'], + // testMatch: ['**/utils/common.test.ts'], + // testMatch: ['**/*/leaf.test.tsx'], + // testMatch: ['**/*/is-use-loop.test.ts'], transformIgnorePatterns: [ `/node_modules/(?!${esModules})/`, ], diff --git a/packages/renderer-core/package.json b/packages/renderer-core/package.json index 7b770f443e..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.0.15", + "version": "1.3.2", "description": "renderer core", "license": "MIT", "main": "lib/index.js", @@ -10,14 +10,14 @@ "es" ], "scripts": { - "build": "build-scripts build --skip-demo", + "build": "build-scripts build", "test": "build-scripts test --config build.test.json", "test:cov": "build-scripts test --config build.test.json --jest-coverage" }, "dependencies": { "@alilc/lowcode-datasource-engine": "^1.0.0", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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", @@ -27,14 +27,12 @@ "prop-types": "^15.7.2", "react-is": "^16.10.1", "socket.io-client": "^2.2.0", - "whatwg-fetch": "^3.0.0", - "zen-logger": "^1.1.4" + "whatwg-fetch": "^3.0.0" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", "@alifd/next": "^1.26.0", - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-test-mate": "^1.0.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", @@ -45,8 +43,6 @@ "@types/prop-types": "^15.7.3", "@types/react-is": "^17.0.3", "@types/react-test-renderer": "^17.0.1", - "babel-jest": "^26.5.2", - "build-plugin-component": "^0.2.11", "jest": "^26.6.3", "react-test-renderer": "^17.0.2", "ts-jest": "^26.5.0" @@ -59,5 +55,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/renderer-core" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/renderer-core/src/adapter/index.ts b/packages/renderer-core/src/adapter/index.ts index 12896b1372..7a56bc039e 100644 --- a/packages/renderer-core/src/adapter/index.ts +++ b/packages/renderer-core/src/adapter/index.ts @@ -2,7 +2,6 @@ import { IRuntime, IRendererModules, IGeneralConstructor } from '../types'; export enum Env { React = 'react', - Rax = 'rax', } class Adapter { @@ -22,22 +21,22 @@ class Adapter { initRuntime() { const Component: IGeneralConstructor = class <T = any, S = any> { - setState() {} - forceUpdate() {} - render() {} state: Readonly<S>; props: Readonly<T> & Readonly<{ children?: any | undefined }>; refs: Record<string, unknown>; context: Record<string, unknown>; - }; - const PureComponent = class <T = any, S = any> { setState() {} forceUpdate() {} render() {} + }; + const PureComponent = class <T = any, S = any> { state: Readonly<S>; props: Readonly<T> & Readonly<{ children?: any | undefined }>; refs: Record<string, unknown>; context: Record<string, unknown>; + setState() {} + forceUpdate() {} + render() {} }; const createElement = () => {}; const createContext = () => {}; @@ -85,10 +84,6 @@ class Adapter { return this.env === Env.React; } - isRax() { - return this.env === Env.Rax; - } - setRenderers(renderers: IRendererModules) { this.renderers = renderers; } diff --git a/packages/renderer-core/src/hoc/index.tsx b/packages/renderer-core/src/hoc/index.tsx index a9314060f3..4851ea486f 100644 --- a/packages/renderer-core/src/hoc/index.tsx +++ b/packages/renderer-core/src/hoc/index.tsx @@ -1,20 +1,88 @@ import { cloneEnumerableProperty } from '@alilc/lowcode-utils'; import adapter from '../adapter'; +import { IBaseRendererInstance, IRendererProps } from '../types'; -export function compWrapper(Comp: any) { +interface Options { + baseRenderer: IBaseRendererInstance; + schema: any; +} + +function patchDidCatch(Comp: any, { baseRenderer }: Options) { + if (Comp.patchedCatch) { + return; + } + Comp.patchedCatch = true; + const { PureComponent } = adapter.getRuntime(); + // Rax 的 getDerivedStateFromError 有 BUG,这里先用 componentDidCatch 来替代 + // @see https://github.com/alibaba/rax/issues/2211 + const originalDidCatch = Comp.prototype.componentDidCatch; + Comp.prototype.componentDidCatch = function didCatch(this: any, error: Error, errorInfo: any) { + this.setState({ engineRenderError: true, error }); + if (originalDidCatch && typeof originalDidCatch === 'function') { + originalDidCatch.call(this, error, errorInfo); + } + }; + + const { engine } = baseRenderer.context; + const originRender = Comp.prototype.render; + Comp.prototype.render = function () { + if (this.state && this.state.engineRenderError) { + this.state.engineRenderError = false; + return engine.createElement(engine.getFaultComponent(), { + ...this.props, + error: this.state.error, + componentName: this.props._componentName, + }); + } + return originRender.call(this); + }; + if (!(Comp.prototype instanceof PureComponent)) { + const originShouldComponentUpdate = Comp.prototype.shouldComponentUpdate; + Comp.prototype.shouldComponentUpdate = function (nextProps: IRendererProps, nextState: any) { + if (nextState && nextState.engineRenderError) { + return true; + } + return originShouldComponentUpdate + ? originShouldComponentUpdate.call(this, nextProps, nextState) + : true; + }; + } +} + +const cache = new Map<string, { Comp: any; WrapperComponent: any }>(); + +export function compWrapper(Comp: any, options: Options) { const { createElement, Component, forwardRef } = adapter.getRuntime(); - class Wrapper extends Component { - // constructor(props: any, context: any) { - // super(props, context); - // } + if ( + Comp?.prototype?.isReactComponent || // react + Comp?.prototype?.setState || // rax + Comp?.prototype instanceof Component + ) { + patchDidCatch(Comp, options); + return Comp; + } + + if (cache.has(options.schema.id) && cache.get(options.schema.id)?.Comp === Comp) { + return cache.get(options.schema.id)?.WrapperComponent; + } + class Wrapper extends Component { render() { - return createElement(Comp, this.props); + return createElement(Comp, { ...this.props, ref: this.props.forwardRef }); } } (Wrapper as any).displayName = Comp.displayName; - return cloneEnumerableProperty(forwardRef((props: any, ref: any) => { - return createElement(Wrapper, { ...props, forwardRef: ref }); - }), Comp); + patchDidCatch(Wrapper, options); + + const WrapperComponent = cloneEnumerableProperty( + forwardRef((props: any, ref: any) => { + return createElement(Wrapper, { ...props, forwardRef: ref }); + }), + Comp, + ); + + cache.set(options.schema.id, { WrapperComponent, Comp }); + + return WrapperComponent; } diff --git a/packages/renderer-core/src/hoc/leaf.tsx b/packages/renderer-core/src/hoc/leaf.tsx index d4fa2afefd..2bb3c0b368 100644 --- a/packages/renderer-core/src/hoc/leaf.tsx +++ b/packages/renderer-core/src/hoc/leaf.tsx @@ -1,10 +1,10 @@ -import { BuiltinSimulatorHost, Node, PropChangeOptions } from '@alilc/lowcode-designer'; -import { GlobalEvent, TransformStage, NodeSchema } from '@alilc/lowcode-types'; +import { INode, IPublicTypePropChangeOptions } from '@alilc/lowcode-designer'; +import { GlobalEvent, IPublicEnumTransformStage, IPublicTypeNodeSchema, IPublicTypeEngineOptions } from '@alilc/lowcode-types'; import { isReactComponent, cloneEnumerableProperty } from '@alilc/lowcode-utils'; -import { EngineOptions } from '@alilc/lowcode-editor-core'; import { debounce } from '../utils/common'; import adapter from '../adapter'; import * as types from '../types/index'; +import logger from '../utils/logger'; export interface IComponentHocInfo { schema: any; @@ -17,21 +17,23 @@ export interface IComponentHocProps { __tag: any; componentId: any; _leaf: any; - forwardedRef: any; + forwardedRef?: any; } export interface IComponentHocState { childrenInState: boolean; nodeChildren: any; nodeCacheProps: any; + /** 控制是否显示隐藏 */ visible: boolean; + /** 控制是否渲染 */ condition: boolean; nodeProps: any; } -type DesignMode = Pick<EngineOptions, 'designMode'>['designMode']; +type DesignMode = Pick<IPublicTypeEngineOptions, 'designMode'>['designMode']; export interface IComponentHoc { designMode: DesignMode | DesignMode[]; @@ -41,15 +43,17 @@ export interface IComponentHoc { export type IComponentConstruct = (Comp: types.IBaseRenderComponent, info: IComponentHocInfo) => types.IGeneralConstructor; interface IProps { - _leaf: Node | undefined; + _leaf: INode | undefined; visible: boolean; - componentId?: number; + componentId: number; + + children?: INode[]; - children?: Node[]; + __tag: number; - __tag?: number; + forwardedRef?: any; } enum RerenderType { @@ -62,8 +66,7 @@ enum RerenderType { // 缓存 Leaf 层组件,防止重新渲染问题 class LeafCache { - constructor(public documentId: string, public device: string) { - } + /** 组件缓存 */ component = new Map(); @@ -78,6 +81,9 @@ class LeafCache { event = new Map(); ref = new Map(); + + constructor(public documentId: string, public device: string) { + } } let cache: LeafCache; @@ -97,21 +103,33 @@ function initRerenderEvent({ return; } cache.event.get(schema.id)?.dispose.forEach((disposeFn: any) => disposeFn && disposeFn()); + const debounceRerender = debounce(() => { + container.rerender(); + }, 20); cache.event.set(schema.id, { clear: false, leaf, dispose: [ leaf?.onPropChange?.(() => { + if (!container.autoRepaintNode) { + return; + } __debug(`${schema.componentName}[${schema.id}] leaf not render in SimulatorRendererView, leaf onPropsChange make rerender`); - container.rerender(); + debounceRerender(); }), leaf?.onChildrenChange?.(() => { + if (!container.autoRepaintNode) { + return; + } __debug(`${schema.componentName}[${schema.id}] leaf not render in SimulatorRendererView, leaf onChildrenChange make rerender`); - container.rerender(); + debounceRerender(); }) as Function, leaf?.onVisibleChange?.(() => { + if (!container.autoRepaintNode) { + return; + } __debug(`${schema.componentName}[${schema.id}] leaf not render in SimulatorRendererView, leaf onVisibleChange make rerender`); - container.rerender(); + debounceRerender(); }), ], }); @@ -147,11 +165,11 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { const curDocumentId = baseRenderer.props?.documentId ?? ''; const curDevice = baseRenderer.props?.device ?? ''; const getNode = baseRenderer.props?.getNode; - const container: BuiltinSimulatorHost = baseRenderer.props?.__container; + const container = baseRenderer.props?.__container; const setSchemaChangedSymbol = baseRenderer.props?.setSchemaChangedSymbol; const editor = host?.designer?.editor; const runtime = adapter.getRuntime(); - const { forwardRef } = runtime; + const { forwardRef, createElement } = runtime; const Component = runtime.Component as types.IGeneralConstructor< IComponentHocProps, IComponentHocState >; @@ -166,7 +184,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { } if (!isReactComponent(Comp)) { - console.error(`${schema.componentName} component may be has errors: `, Comp); + logger.error(`${schema.componentName} component may be has errors: `, Comp); } initRerenderEvent({ @@ -176,22 +194,72 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { getNode, }); - if (curDocumentId && cache.component.has(componentCacheId)) { - return cache.component.get(componentCacheId); + if (curDocumentId && cache.component.has(componentCacheId) && (cache.component.get(componentCacheId).Comp === Comp)) { + return cache.component.get(componentCacheId).LeafWrapper; } class LeafHoc extends Component { recordInfo: { startTime?: number | null; type?: string; - node?: Node; + node?: INode; } = {}; + + private curEventLeaf: INode | undefined; + static displayName = schema.componentName; disposeFunctions: Array<((() => void) | Function)> = []; __component_tag = 'leafWrapper'; + renderUnitInfo: { + minimalUnitId?: string; + minimalUnitName?: string; + singleRender?: boolean; + }; + + // 最小渲染单元做防抖处理 + makeUnitRenderDebounced = debounce(() => { + this.beforeRender(RerenderType.MinimalRenderUnit); + const schema = this.leaf?.export?.(IPublicEnumTransformStage.Render); + if (!schema) { + return; + } + const nextProps = getProps(schema, scope, Comp, componentInfo); + const children = getChildren(schema, scope, Comp); + const nextState = { + nodeProps: nextProps, + nodeChildren: children, + childrenInState: true, + }; + if ('children' in nextProps) { + nextState.nodeChildren = nextProps.children; + } + + __debug(`${this.leaf?.componentName}(${this.props.componentId}) MinimalRenderUnit Render!`); + this.setState(nextState); + }, 20); + + constructor(props: IProps, context: any) { + super(props, context); + // 监听以下事件,当变化时更新自己 + __debug(`${schema.componentName}[${this.props.componentId}] leaf render in SimulatorRendererView`); + clearRerenderEvent(componentCacheId); + this.curEventLeaf = this.leaf; + + cache.ref.set(componentCacheId, { + makeUnitRender: this.makeUnitRender, + }); + + let cacheState = cache.state.get(componentCacheId); + if (!cacheState || cacheState.__tag !== props.__tag) { + cacheState = this.getDefaultState(props); + } + + this.state = cacheState; + } + recordTime = () => { if (!this.recordInfo.startTime) { return; @@ -199,7 +267,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { const endTime = Date.now(); const nodeCount = host?.designer?.currentDocument?.getNodeCount?.(); const componentName = this.recordInfo.node?.componentName || this.leaf?.componentName || 'UnknownComponent'; - editor?.emit(GlobalEvent.Node.Rerender, { + editor?.eventBus.emit(GlobalEvent.Node.Rerender, { componentName, time: endTime - this.recordInfo.startTime, type: this.recordInfo.type, @@ -208,19 +276,31 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { this.recordInfo.startTime = null; }; + makeUnitRender = () => { + this.makeUnitRenderDebounced(); + }; + + get autoRepaintNode() { + return container?.autoRepaintNode; + } + componentDidUpdate() { this.recordTime(); } componentDidMount() { + const _leaf = this.leaf; + this.initOnPropsChangeEvent(_leaf); + this.initOnChildrenChangeEvent(_leaf); + this.initOnVisibleChangeEvent(_leaf); this.recordTime(); } - get defaultState() { + getDefaultState(nextProps: any) { const { hidden = false, condition = true, - } = this.leaf?.export?.(TransformStage.Render) || {}; + } = nextProps.__inner__ || this.leaf?.export?.(IPublicEnumTransformStage.Render) || {}; return { nodeChildren: null, childrenInState: false, @@ -231,31 +311,6 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { }; } - constructor(props: IProps, context: any) { - super(props, context); - // 监听以下事件,当变化时更新自己 - __debug(`${schema.componentName}[${this.props.componentId}] leaf render in SimulatorRendererView`); - clearRerenderEvent(componentCacheId); - const _leaf = this.leaf; - this.initOnPropsChangeEvent(_leaf); - this.initOnChildrenChangeEvent(_leaf); - this.initOnVisibleChangeEvent(_leaf); - this.curEventLeaf = _leaf; - - cache.ref.set(componentCacheId, { - makeUnitRender: this.makeUnitRender, - }); - - let cacheState = cache.state.get(componentCacheId); - if (!cacheState || cacheState.__tag !== props.__tag) { - cacheState = this.defaultState; - } - - this.state = cacheState; - } - - private curEventLeaf: Node | undefined; - setState(state: any) { cache.state.set(componentCacheId, { ...this.state, @@ -266,19 +321,13 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { } /** 由于内部属性变化,在触发渲染前,会执行该函数 */ - beforeRender(type: string, node?: Node): void { + beforeRender(type: string, node?: INode): void { this.recordInfo.startTime = Date.now(); this.recordInfo.type = type; this.recordInfo.node = node; setSchemaChangedSymbol?.(true); } - renderUnitInfo: { - minimalUnitId?: string; - minimalUnitName?: string; - singleRender?: boolean; - }; - judgeMiniUnitRender() { if (!this.renderUnitInfo) { this.getRenderUnitInfo(); @@ -296,7 +345,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { if (!ref) { __debug('Cant find minimalRenderUnit ref! This make rerender!'); - container.rerender(); + container?.rerender(); return; } __debug(`${this.leaf?.componentName}(${this.props.componentId}) need render, make its minimalRenderUnit ${renderUnitInfo.minimalUnitName}(${renderUnitInfo.minimalUnitId})`); @@ -309,7 +358,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { return; } - if (leaf.isRoot()) { + if (leaf.isRootNode) { this.renderUnitInfo = { singleRender: true, ...(this.renderUnitInfo || {}), @@ -335,32 +384,6 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { } } - // 最小渲染单元做防抖处理 - makeUnitRenderDebounced = debounce(() => { - this.beforeRender(RerenderType.MinimalRenderUnit); - const schema = this.leaf?.export?.(TransformStage.Render); - if (!schema) { - return; - } - const nextProps = getProps(schema, scope, Comp, componentInfo); - const children = getChildren(schema, scope, Comp); - const nextState = { - nodeProps: nextProps, - nodeChildren: children, - childrenInState: true, - }; - if ('children' in nextProps) { - nextState.nodeChildren = nextProps.children; - } - - __debug(`${this.leaf?.componentName}(${this.props.componentId}) MinimalRenderUnit Render!`); - this.setState(nextState); - }, 20); - - makeUnitRender = () => { - this.makeUnitRenderDebounced(); - }; - componentWillReceiveProps(nextProps: any) { let { componentId } = nextProps; if (nextProps.__tag === this.props.__tag) { @@ -380,13 +403,13 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { const { visible, ...resetState - } = this.defaultState; + } = this.getDefaultState(nextProps); this.setState(resetState); } /** 监听参数变化 */ initOnPropsChangeEvent(leaf = this.leaf): void { - const dispose = leaf?.onPropChange?.((propChangeInfo: PropChangeOptions) => { + const handlePropsChange = debounce((propChangeInfo: IPublicTypePropChangeOptions) => { const { key, newValue = null, @@ -394,7 +417,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { const node = leaf; if (key === '___condition___') { - const { condition = true } = this.leaf?.export(TransformStage.Render) || {}; + const { condition = true } = this.leaf?.export(IPublicEnumTransformStage.Render) || {}; const conditionValue = __parseData?.(condition, scope); __debug(`key is ___condition___, change condition value to [${condition}]`); // 条件表达式改变 @@ -408,7 +431,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { // 目前多层循坏无法判断需要从哪一层开始渲染,故先粗暴解决 if (key === '___loop___') { __debug('key is ___loop___, render a page!'); - container.rerender(); + container?.rerender(); // 由于 scope 变化,需要清空缓存,使用新的 scope cache.component.delete(componentCacheId); return; @@ -416,7 +439,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { this.beforeRender(RerenderType.PropsChanged); const { state } = this; const { nodeCacheProps } = state; - const nodeProps = getProps(node?.export?.(TransformStage.Render) as NodeSchema, scope, Comp, componentInfo); + const nodeProps = getProps(node?.export?.(IPublicEnumTransformStage.Render) as IPublicTypeNodeSchema, scope, Comp, componentInfo); if (key && !(key in nodeProps) && (key in this.props)) { // 当 key 在 this.props 中时,且不存在在计算值中,需要用 newValue 覆盖掉 this.props 的取值 nodeCacheProps[key] = newValue; @@ -434,6 +457,12 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { this.judgeMiniUnitRender(); }); + const dispose = leaf?.onPropChange?.((propChangeInfo: IPublicTypePropChangeOptions) => { + if (!this.autoRepaintNode) { + return; + } + handlePropsChange(propChangeInfo); + }); dispose && this.disposeFunctions.push(dispose); } @@ -443,6 +472,9 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { */ initOnVisibleChangeEvent(leaf = this.leaf) { const dispose = leaf?.onVisibleChange?.((flag: boolean) => { + if (!this.autoRepaintNode) { + return; + } if (this.state.visible === flag) { return; } @@ -463,6 +495,9 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { */ initOnChildrenChangeEvent(leaf = this.leaf) { const dispose = leaf?.onChildrenChange?.((param): void => { + if (!this.autoRepaintNode) { + return; + } const { type, node, @@ -471,7 +506,7 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { // TODO: 缓存同级其他元素的 children。 // 缓存二级 children Next 查询筛选组件有问题 // 缓存一级 children Next Tab 组件有问题 - const nextChild = getChildren(leaf?.export?.(TransformStage.Render) as types.ISchema, scope, Comp); + const nextChild = getChildren(leaf?.export?.(IPublicEnumTransformStage.Render) as types.ISchema, scope, Comp); __debug(`${schema.componentName}[${this.props.componentId}] component trigger onChildrenChange event`, nextChild); this.setState({ nodeChildren: nextChild, @@ -487,16 +522,11 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { } get hasChildren(): boolean { - let { children } = this.props; - if (this.state.childrenInState) { - children = this.state.nodeChildren; + if (!this.state.childrenInState) { + return 'children' in this.props; } - if (Array.isArray(children)) { - return Boolean(children && children.length); - } - - return Boolean(children); + return true; } get children(): any { @@ -509,10 +539,10 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { if (this.props.children && this.props.children.length) { return this.props.children; } - return []; + return this.props.children; } - get leaf(): Node | undefined { + get leaf(): INode | undefined { if (this.props._leaf?.isMock) { // 低代码组件作为一个整体更新,其内部的组件不需要监听相关事件 return undefined; @@ -540,23 +570,31 @@ export function leafWrapper(Comp: types.IBaseRenderComponent, { ref: forwardedRef, }; - return engine.createElement(Comp, compProps, this.hasChildren ? this.children : null); + delete compProps.__inner__; + + if (this.hasChildren) { + return engine.createElement(Comp, compProps, this.children); + } + + return engine.createElement(Comp, compProps); } } - let LeafWrapper = forwardRef((props: any, ref: any) => ( - // @ts-ignore - <LeafHoc - {...props} - forwardedRef={ref} - /> - )); + let LeafWrapper = forwardRef((props: any, ref: any) => { + return createElement(LeafHoc, { + ...props, + forwardedRef: ref, + }); + }); LeafWrapper = cloneEnumerableProperty(LeafWrapper, Comp); LeafWrapper.displayName = (Comp as any).displayName; - cache.component.set(componentCacheId, LeafWrapper); + cache.component.set(componentCacheId, { + LeafWrapper, + Comp, + }); return LeafWrapper; } \ No newline at end of file diff --git a/packages/renderer-core/src/renderer/addon.tsx b/packages/renderer-core/src/renderer/addon.tsx index 62aeddbbad..211ec182f2 100644 --- a/packages/renderer-core/src/renderer/addon.tsx +++ b/packages/renderer-core/src/renderer/addon.tsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import baseRendererFactory from './base'; import { isEmpty } from '../utils'; import { IRendererAppHelper, IBaseRendererProps, IBaseRenderComponent } from '../types'; +import logger from '../utils/logger'; export default function addonRendererFactory(): IBaseRenderComponent { const BaseRenderer = baseRendererFactory(); @@ -32,7 +33,7 @@ export default function addonRendererFactory(): IBaseRenderComponent { const schema = props.__schema || {}; this.state = this.__parseData(schema.state || {}); if (isEmpty(props.config) || !props.config?.addonKey) { - console.warn('lce addon has wrong config'); + logger.warn('lce addon has wrong config'); this.setState({ __hasError: true, }); @@ -45,7 +46,7 @@ export default function addonRendererFactory(): IBaseRenderComponent { this.__initDataSource(props); this.open = this.open || (() => { }); this.close = this.close || (() => { }); - this.__excuteLifeCycleMethod('constructor', [...arguments]); + this.__executeLifeCycleMethod('constructor', [...arguments]); } async componentWillUnmount() { diff --git a/packages/renderer-core/src/renderer/base.tsx b/packages/renderer-core/src/renderer/base.tsx index 2d58cac764..d240095604 100644 --- a/packages/renderer-core/src/renderer/base.tsx +++ b/packages/renderer-core/src/renderer/base.tsx @@ -3,7 +3,8 @@ /* eslint-disable react/prop-types */ import classnames from 'classnames'; import { create as createDataSourceEngine } from '@alilc/lowcode-datasource-engine/interpret'; -import { isI18nData, isJSExpression, isJSFunction, NodeSchema, NodeData, JSONValue, CompositeValue } from '@alilc/lowcode-types'; +import { IPublicTypeNodeSchema, IPublicTypeNodeData, IPublicTypeJSONValue, IPublicTypeCompositeValue } from '@alilc/lowcode-types'; +import { checkPropTypes, isI18nData, isJSExpression, isJSFunction } from '@alilc/lowcode-utils'; import adapter from '../adapter'; import divFactory from '../components/Div'; import visualDomFactory from '../components/VisualDom'; @@ -20,16 +21,14 @@ import { isFileSchema, transformArrayToMap, transformStringToFunction, - checkPropTypes, getI18n, - canAcceptsRef, getFileCssName, capitalizeFirstLetter, DataHelper, isVariable, isJSSlot, } from '../utils'; -import { IBaseRendererProps, INodeInfo, IBaseRenderComponent, IBaseRendererContext, IGeneralConstructor, IRendererAppHelper, DataSource } from '../types'; +import { IBaseRendererProps, INodeInfo, IBaseRenderComponent, IBaseRendererContext, IRendererAppHelper, DataSource } from '../types'; import { compWrapper } from '../hoc'; import { IComponentConstruct, leafWrapper } from '../hoc/leaf'; import logger from '../utils/logger'; @@ -39,7 +38,7 @@ import isUseLoop from '../utils/is-use-loop'; * execute method in schema.lifeCycles with context * @PRIVATE */ -export function excuteLifeCycleMethod(context: any, schema: NodeSchema, method: string, args: any, thisRequiredInJSE: boolean | undefined): any { +export function executeLifeCycleMethod(context: any, schema: IPublicTypeNodeSchema, method: string, args: any, thisRequiredInJSE: boolean | undefined): any { if (!context || !isSchema(schema) || !method) { return; } @@ -56,14 +55,14 @@ export function excuteLifeCycleMethod(context: any, schema: NodeSchema, method: } if (typeof fn !== 'function') { - console.error(`生命周期${method}类型不符`, fn); + logger.error(`生命周期${method}类型不符`, fn); return; } try { return fn.apply(context, args); } catch (e) { - console.error(`[${schema.componentName}]生命周期${method}出错`, e); + logger.error(`[${schema.componentName}]生命周期${method}出错`, e); } } @@ -71,7 +70,7 @@ export function excuteLifeCycleMethod(context: any, schema: NodeSchema, method: * get children from a node schema * @PRIVATE */ -export function getSchemaChildren(schema: NodeSchema | undefined) { +export function getSchemaChildren(schema: IPublicTypeNodeSchema | undefined) { if (!schema) { return; } @@ -88,7 +87,7 @@ export function getSchemaChildren(schema: NodeSchema | undefined) { return schema.children; } - let result = ([] as NodeData[]).concat(schema.children); + let result = ([] as IPublicTypeNodeData[]).concat(schema.children); if (Array.isArray(schema.props.children)) { result = result.concat(schema.props.children); } else { @@ -120,6 +119,8 @@ export default function baseRendererFactory(): IBaseRenderComponent { let scopeIdx = 0; return class BaseRenderer extends Component<IBaseRendererProps, Record<string, any>> { + [key: string]: any; + static displayName = 'BaseRenderer'; static defaultProps = { @@ -128,7 +129,6 @@ export default function baseRendererFactory(): IBaseRenderComponent { static contextType = AppContext; - appHelper?: IRendererAppHelper; i18n: any; getLocale: any; setLocale: any; @@ -138,6 +138,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { __compScopes: Record<string, any> = {}; __instanceMap: Record<string, any> = {}; __dataHelper: any; + /** * keep track of customMethods added to this context * @@ -154,12 +155,12 @@ export default function baseRendererFactory(): IBaseRenderComponent { */ __styleElement: any; - [key: string]: any; - constructor(props: IBaseRendererProps, context: IBaseRendererContext) { super(props, context); this.context = context; - this.__parseExpression = props?.thisRequiredInJSE ? parseThisRequiredExpression : parseExpression; + this.__parseExpression = (str: string, self: any) => { + return parseExpression({ str, self, thisRequired: props?.thisRequiredInJSE, logScope: props.componentName }); + }; this.__beforeInit(props); this.__init(props); this.__afterInit(props); @@ -170,44 +171,44 @@ export default function baseRendererFactory(): IBaseRenderComponent { __beforeInit(_props: IBaseRendererProps) { } __init(props: IBaseRendererProps) { - this.appHelper = props.__appHelper; this.__compScopes = {}; this.__instanceMap = {}; this.__bindCustomMethods(props); - this.__initI18nAPIs(props); + this.__initI18nAPIs(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars __afterInit(_props: IBaseRendererProps) { } static getDerivedStateFromProps(props: IBaseRendererProps, state: any) { - return excuteLifeCycleMethod(this, props?.__schema, 'getDerivedStateFromProps', [props, state], props.thisRequiredInJSE); + const result = executeLifeCycleMethod(this, props?.__schema, 'getDerivedStateFromProps', [props, state], props.thisRequiredInJSE); + return result === undefined ? null : result; } async getSnapshotBeforeUpdate(...args: any[]) { - this.__excuteLifeCycleMethod('getSnapshotBeforeUpdate', args); + this.__executeLifeCycleMethod('getSnapshotBeforeUpdate', args); this.__debug(`getSnapshotBeforeUpdate - ${this.props?.__schema?.fileName}`); } async componentDidMount(...args: any[]) { this.reloadDataSource(); - this.__excuteLifeCycleMethod('componentDidMount', args); + this.__executeLifeCycleMethod('componentDidMount', args); this.__debug(`componentDidMount - ${this.props?.__schema?.fileName}`); } async componentDidUpdate(...args: any[]) { - this.__excuteLifeCycleMethod('componentDidUpdate', args); + this.__executeLifeCycleMethod('componentDidUpdate', args); this.__debug(`componentDidUpdate - ${this.props.__schema.fileName}`); } async componentWillUnmount(...args: any[]) { - this.__excuteLifeCycleMethod('componentWillUnmount', args); + this.__executeLifeCycleMethod('componentWillUnmount', args); this.__debug(`componentWillUnmount - ${this.props?.__schema?.fileName}`); } async componentDidCatch(...args: any[]) { - this.__excuteLifeCycleMethod('componentDidCatch', args); - console.warn(args); + this.__executeLifeCycleMethod('componentDidCatch', args); + logger.warn(args); } reloadDataSource = () => new Promise((resolve, reject) => { @@ -241,12 +242,13 @@ export default function baseRendererFactory(): IBaseRenderComponent { super.forceUpdate(); } } + /** * execute method in schema.lifeCycles * @PRIVATE */ - __excuteLifeCycleMethod = (method: string, args?: any) => { - excuteLifeCycleMethod(this, this.props.__schema, method, args, this.props.thisRequiredInJSE); + __executeLifeCycleMethod = (method: string, args?: any) => { + executeLifeCycleMethod(this, this.props.__schema, method, args, this.props.thisRequiredInJSE); }; /** @@ -276,7 +278,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { value = this.__parseExpression(value, this); } if (typeof value !== 'function') { - console.error(`custom method ${key} can not be parsed to a valid function`, value); + logger.error(`custom method ${key} can not be parsed to a valid function`, value); return; } this[key] = value.bind(this); @@ -296,8 +298,8 @@ export default function baseRendererFactory(): IBaseRenderComponent { }; __parseData = (data: any, ctx?: Record<string, any>) => { - const { __ctx, thisRequiredInJSE } = this.props; - return parseData(data, ctx || __ctx || this, { thisRequiredInJSE }); + const { __ctx, thisRequiredInJSE, componentName } = this.props; + return parseData(data, ctx || __ctx || this, { thisRequiredInJSE, logScope: componentName }); }; __initDataSource = (props: IBaseRendererProps) => { @@ -358,16 +360,16 @@ export default function baseRendererFactory(): IBaseRenderComponent { * init i18n apis * @PRIVATE */ - __initI18nAPIs = (props: IBaseRendererProps) => { + __initI18nAPIs = () => { this.i18n = (key: string, values = {}) => { - const { locale, messages } = props; + const { locale, messages } = this.props; return getI18n(key, values, locale, messages); }; - this.getLocale = () => props.locale; + this.getLocale = () => this.props.locale; this.setLocale = (loc: string) => { const setLocaleFn = this.appHelper?.utils?.i18n?.setLocale; if (!setLocaleFn || typeof setLocaleFn !== 'function') { - console.warn('initI18nAPIs Failed, i18n only works when appHelper.utils.i18n.setLocale() exists'); + logger.warn('initI18nAPIs Failed, i18n only works when appHelper.utils.i18n.setLocale() exists'); return undefined; } return setLocaleFn(loc); @@ -403,7 +405,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { __render = () => { const schema = this.props.__schema; - this.__excuteLifeCycleMethod('render'); + this.__executeLifeCycleMethod('render'); this.__writeCss(this.props); const { engine } = this.context; @@ -426,7 +428,14 @@ export default function baseRendererFactory(): IBaseRenderComponent { __createDom = () => { const { __schema, __ctx, __components = {} } = this.props; - const scope: any = {}; + // merge defaultProps + const scopeProps = { + ...__schema.defaultProps, + ...this.props, + }; + const scope: any = { + props: scopeProps, + }; scope.__proto__ = __ctx || this; const _children = getSchemaChildren(__schema); @@ -449,8 +458,8 @@ export default function baseRendererFactory(): IBaseRenderComponent { * @param parentInfo 父组件的信息,包含schema和Comp * @param idx 为循环渲染的循环Index */ - __createVirtualDom = (originalSchema: NodeData | NodeData[] | undefined, originalScope: any, parentInfo: INodeInfo, idx: string | number = ''): any => { - if (!originalSchema) { + __createVirtualDom = (originalSchema: IPublicTypeNodeData | IPublicTypeNodeData[] | undefined, originalScope: any, parentInfo: INodeInfo, idx: string | number = ''): any => { + if (originalSchema === null || originalSchema === undefined) { return null; } let scope = originalScope; @@ -485,10 +494,19 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (schema.length === 1) { return this.__createVirtualDom(schema[0], scope, parentInfo); } - return schema.map((item, idy) => this.__createVirtualDom(item, scope, parentInfo, (item as NodeSchema)?.__ctx?.lceKey ? '' : String(idy))); + return schema.map((item, idy) => this.__createVirtualDom(item, scope, parentInfo, (item as IPublicTypeNodeSchema)?.__ctx?.lceKey ? '' : String(idy))); + } + + // @ts-expect-error 如果直接转换好了,可以返回 + if (schema.$$typeof) { + return schema; } const _children = getSchemaChildren(schema); + if (!schema.componentName) { + logger.error('The componentName in the schema is invalid, please check the schema: ', schema); + return; + } // 解析占位组件 if (schema.componentName === 'Fragment' && _children) { const tarChildren = isJSExpression(_children) ? this.__parseExpression(_children, scope) : _children; @@ -501,11 +519,6 @@ export default function baseRendererFactory(): IBaseRenderComponent { schema.children = [text]; } - // @ts-expect-error 如果直接转换好了,可以返回 - if (schema.$$typeof) { - return schema; - } - if (!isSchema(schema)) { return null; } @@ -521,7 +534,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { : {}; if (!Comp) { - console.error(`${schema.componentName} component is not found in components list! component list is:`, components || this.props.__container?.components); + logger.error(`${schema.componentName} component is not found in components list! component list is:`, components || this.props.__container?.components); return engine.createElement( engine.getNotFoundComponent(), { @@ -538,6 +551,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { if (schema.loop != null) { const loop = this.__parseData(schema.loop, scope); + if (Array.isArray(loop) && loop.length === 0) return null; const useLoop = isUseLoop(loop, this.__designModeIsDesign); if (useLoop) { return this.__createLoopVirtualDom( @@ -608,12 +622,6 @@ export default function baseRendererFactory(): IBaseRenderComponent { }); }); - // 对于不可以接收到 ref 的组件需要做特殊处理 - if (!canAcceptsRef(Comp)) { - Comp = compWrapper(Comp); - components[schema.componentName] = Comp; - } - otherProps.ref = (ref: any) => { this.$(props.fieldId || props.ref, ref); // 收集ref const refProps = props.ref; @@ -642,7 +650,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { props.key = props.__id; } - let child = this.__getSchemaChildrenVirtualDom(schema, scope, Comp); + let child = this.__getSchemaChildrenVirtualDom(schema, scope, Comp, condition); const renderComp = (innerProps: any) => engine.createElement(Comp, innerProps, child); // 设计模式下的特殊处理 if (engine && [DESIGN_MODE.EXTEND, DESIGN_MODE.BORDER].includes(engine.props.designMode)) { @@ -670,7 +678,14 @@ export default function baseRendererFactory(): IBaseRenderComponent { } } } - return renderComp({ ...props, ...otherProps }); + return renderComp({ + ...props, + ...otherProps, + __inner__: { + hidden: schema.hidden, + condition, + }, + }); } catch (e) { return engine.createElement(engine.getFaultComponent(), { error: e, @@ -690,13 +705,13 @@ export default function baseRendererFactory(): IBaseRenderComponent { */ get __componentHOCs(): IComponentConstruct[] { if (this.__designModeIsDesign) { - return [leafWrapper]; + return [leafWrapper, compWrapper]; } - return []; + return [compWrapper]; } - __getSchemaChildrenVirtualDom = (schema: NodeSchema | undefined, scope: any, Comp: any) => { - let children = getSchemaChildren(schema); + __getSchemaChildrenVirtualDom = (schema: IPublicTypeNodeSchema | undefined, scope: any, Comp: any, condition = true) => { + let children = condition ? getSchemaChildren(schema) : null; // @todo 补完这里的 Element 定义 @承虎 let result: any = []; @@ -725,7 +740,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { return null; }; - __getComponentProps = (schema: NodeSchema | undefined, scope: any, Comp: any, componentInfo?: any) => { + __getComponentProps = (schema: IPublicTypeNodeSchema | undefined, scope: any, Comp: any, componentInfo?: any) => { if (!schema) { return {}; } @@ -739,9 +754,9 @@ export default function baseRendererFactory(): IBaseRenderComponent { }) || {}; }; - __createLoopVirtualDom = (schema: NodeSchema, scope: any, parentInfo: INodeInfo, idx: number | string) => { + __createLoopVirtualDom = (schema: IPublicTypeNodeSchema, scope: any, parentInfo: INodeInfo, idx: number | string) => { if (isFileSchema(schema)) { - console.warn('file type not support Loop'); + logger.warn('file type not support Loop'); return null; } if (!Array.isArray(schema.loop)) { @@ -750,7 +765,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { const itemArg = (schema.loopArgs && schema.loopArgs[0]) || DEFAULT_LOOP_ARG_ITEM; const indexArg = (schema.loopArgs && schema.loopArgs[1]) || DEFAULT_LOOP_ARG_INDEX; const { loop } = schema; - return loop.map((item: JSONValue | CompositeValue, i: number) => { + return loop.map((item: IPublicTypeJSONValue | IPublicTypeCompositeValue, i: number) => { const loopSelf: any = { [itemArg]: item, [indexArg]: i, @@ -760,6 +775,11 @@ export default function baseRendererFactory(): IBaseRenderComponent { { ...schema, loop: undefined, + props: { + ...schema.props, + // 循环下 key 不能为常量,这样会造成 key 值重复,渲染异常 + key: isJSExpression(schema.props?.key) ? schema.props?.key : null, + }, }, loopSelf, parentInfo, @@ -816,7 +836,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { } } - const handleI18nData = (innerProps: any) => innerProps[innerProps.use || 'zh_CN']; + const handleI18nData = (innerProps: any) => innerProps[innerProps.use || (this.getLocale && this.getLocale()) || 'zh-CN']; // @LEGACY 兼容老平台设计态 i18n 数据 if (isI18nData(props)) { @@ -888,9 +908,6 @@ export default function baseRendererFactory(): IBaseRenderComponent { }); return checkProps(res); } - if (typeof props === 'string') { - return checkProps(props.trim()); - } return checkProps(props); }; @@ -905,7 +922,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { return this.__instanceMap[filedId]; } - __debug = logger.log; + __debug = (...args: any[]) => { logger.debug(...args); }; __renderContextProvider = (customProps?: object, children?: any) => { return createElement(AppContext.Provider, { @@ -987,7 +1004,7 @@ export default function baseRendererFactory(): IBaseRenderComponent { }, children); } - __checkSchema = (schema: NodeSchema | undefined, originalExtraComponents: string | string[] = []) => { + __checkSchema = (schema: IPublicTypeNodeSchema | undefined, originalExtraComponents: string | string[] = []) => { let extraComponents = originalExtraComponents; if (typeof extraComponents === 'string') { extraComponents = [extraComponents]; @@ -998,6 +1015,10 @@ export default function baseRendererFactory(): IBaseRenderComponent { return !isSchema(schema) || !componentNames.includes(schema?.componentName ?? ''); }; + get appHelper(): IRendererAppHelper { + return this.props.__appHelper; + } + get requestHandlersMap() { return this.appHelper?.requestHandlersMap; } diff --git a/packages/renderer-core/src/renderer/block.tsx b/packages/renderer-core/src/renderer/block.tsx index 560b5924b3..5132997f05 100644 --- a/packages/renderer-core/src/renderer/block.tsx +++ b/packages/renderer-core/src/renderer/block.tsx @@ -13,7 +13,7 @@ export default function blockRendererFactory(): IBaseRenderComponent { const schema = props.__schema || {}; this.state = this.__parseData(schema.state || {}); this.__initDataSource(props); - this.__excuteLifeCycleMethod('constructor', [...arguments]); + this.__executeLifeCycleMethod('constructor', [...arguments]); } render() { diff --git a/packages/renderer-core/src/renderer/component.tsx b/packages/renderer-core/src/renderer/component.tsx index 58d5c0093c..3dfc1df33f 100644 --- a/packages/renderer-core/src/renderer/component.tsx +++ b/packages/renderer-core/src/renderer/component.tsx @@ -15,7 +15,7 @@ export default function componentRendererFactory(): IBaseRenderComponent { const schema = props.__schema || {}; this.state = this.__parseData(schema.state || {}); this.__initDataSource(props); - this.__excuteLifeCycleMethod('constructor', arguments as any); + this.__executeLifeCycleMethod('constructor', arguments as any); } render() { @@ -46,12 +46,5 @@ export default function componentRendererFactory(): IBaseRenderComponent { return this.__renderComp(Component, this.__renderContextProvider({ compContext: this })); } - - /** 需要重载下面几个方法,如果在低代码组件中绑定了对应的生命周期时会出现死循环 */ - componentDidMount() {} - getSnapshotBeforeUpdate() {} - componentDidUpdate() {} - componentWillUnmount() {} - componentDidCatch() {} }; } diff --git a/packages/renderer-core/src/renderer/page.tsx b/packages/renderer-core/src/renderer/page.tsx index ba1140c6bc..16d55e01be 100644 --- a/packages/renderer-core/src/renderer/page.tsx +++ b/packages/renderer-core/src/renderer/page.tsx @@ -1,6 +1,9 @@ +import { getLogger } from '@alilc/lowcode-utils'; import baseRendererFactory from './base'; import { IBaseRendererProps, IBaseRenderComponent } from '../types'; +const logger = getLogger({ level: 'warn', bizName: 'renderer-core:page' }); + export default function pageRendererFactory(): IBaseRenderComponent { const BaseRenderer = baseRendererFactory(); return class PageRenderer extends BaseRenderer { @@ -15,12 +18,12 @@ export default function pageRendererFactory(): IBaseRenderComponent { const schema = props.__schema || {}; this.state = this.__parseData(schema.state || {}); this.__initDataSource(props); - this.__excuteLifeCycleMethod('constructor', [props, ...rest]); + this.__executeLifeCycleMethod('constructor', [props, ...rest]); } async componentDidUpdate(prevProps: IBaseRendererProps, _prevState: {}, snapshot: unknown) { const { __ctx } = this.props; - // 当编排的时候修改schema.state值,需要将最新 schema.state 值 setState + // 当编排的时候修改 schema.state 值,需要将最新 schema.state 值 setState if (JSON.stringify(prevProps.__schema.state) != JSON.stringify(this.props.__schema.state)) { const newState = this.__parseData(this.props.__schema.state, __ctx); this.setState(newState); @@ -29,6 +32,11 @@ export default function pageRendererFactory(): IBaseRenderComponent { super.componentDidUpdate?.(prevProps, _prevState, snapshot); } + setState(state: any, callback?: () => void) { + logger.info('page set state', state); + super.setState(state, callback); + } + render() { const { __schema, __components } = this.props; if (this.__checkSchema(__schema)) { @@ -44,7 +52,6 @@ export default function pageRendererFactory(): IBaseRenderComponent { }); this.__render(); - const { Page } = __components; if (Page) { return this.__renderComp(Page, { pageContext: this }); diff --git a/packages/renderer-core/src/renderer/renderer.tsx b/packages/renderer-core/src/renderer/renderer.tsx index b308c251b8..300b1cd164 100644 --- a/packages/renderer-core/src/renderer/renderer.tsx +++ b/packages/renderer-core/src/renderer/renderer.tsx @@ -5,7 +5,8 @@ import { isFileSchema, isEmpty } from '../utils'; import baseRendererFactory from './base'; import divFactory from '../components/Div'; import { IRenderComponent, IRendererProps, IRendererState } from '../types'; -import { NodeSchema, RootSchema } from '@alilc/lowcode-types'; +import { IPublicTypeNodeSchema, IPublicTypeRootSchema } from '@alilc/lowcode-types'; +import logger from '../utils/logger'; export default function rendererFactory(): IRenderComponent { const { PureComponent, Component, createElement, findDOMNode } = adapter.getRuntime(); @@ -18,10 +19,9 @@ export default function rendererFactory(): IRenderComponent { const debug = Debug('renderer:entry'); - class FaultComponent extends PureComponent<NodeSchema> { + class FaultComponent extends PureComponent<IPublicTypeNodeSchema | any> { render() { - // FIXME: errorlog - console.error('render error', this.props); + logger.error(`%c${this.props.componentName || ''} 组件渲染异常, 异常原因: ${this.props.error?.message || this.props.error || '未知'}`, 'color: #ff0000;'); return createElement(Div, { style: { width: '100%', @@ -59,7 +59,7 @@ export default function rendererFactory(): IRenderComponent { components: {}, designMode: '', suspended: false, - schema: {} as RootSchema, + schema: {} as IPublicTypeRootSchema, onCompGetRef: () => { }, onCompGetCtx: () => { }, thisRequiredInJSE: true, @@ -85,8 +85,9 @@ export default function rendererFactory(): IRenderComponent { debug(`entry.componentWillUnmount - ${this.props?.schema?.componentName}`); } - async componentDidCatch(e: any) { - console.warn(e); + componentDidCatch(error: Error) { + this.state.engineRenderError = true; + this.state.error = error; } shouldComponentUpdate(nextProps: IRendererProps) { @@ -104,52 +105,7 @@ export default function rendererFactory(): IRenderComponent { return SetComponent; } - patchDidCatch(SetComponent: any) { - if (!this.isValidComponent(SetComponent)) { - return; - } - if (SetComponent.patchedCatch) { - return; - } - if (!SetComponent.prototype) { - return; - } - SetComponent.patchedCatch = true; - - // Rax 的 getDerivedStateFromError 有 BUG,这里先用 componentDidCatch 来替代 - // @see https://github.com/alibaba/rax/issues/2211 - const originalDidCatch = SetComponent.prototype.componentDidCatch; - SetComponent.prototype.componentDidCatch = function didCatch(this: any, error: Error, errorInfo: any) { - this.setState({ engineRenderError: true, error }); - if (originalDidCatch && typeof originalDidCatch === 'function') { - originalDidCatch.call(this, error, errorInfo); - } - }; - - const engine = this; - const originRender = SetComponent.prototype.render; - SetComponent.prototype.render = function () { - if (this.state && this.state.engineRenderError) { - this.state.engineRenderError = false; - return engine.createElement(engine.getFaultComponent(), { - ...this.props, - error: this.state.error, - }); - } - return originRender.call(this); - }; - const originShouldComponentUpdate = SetComponent.prototype.shouldComponentUpdate; - SetComponent.prototype.shouldComponentUpdate = function (nextProps: IRendererProps, nextState: any) { - if (nextState && nextState.engineRenderError) { - return true; - } - return originShouldComponentUpdate ? originShouldComponentUpdate.call(this, nextProps, nextState) : true; - }; - } - createElement(SetComponent: any, props: any, children?: any) { - // TODO: enable in runtime mode? - this.patchDidCatch(SetComponent); return (this.props.customCreateElement || createElement)(SetComponent, props, children); } @@ -158,7 +114,25 @@ export default function rendererFactory(): IRenderComponent { } getFaultComponent() { - return this.props.faultComponent || FaultComponent; + const { faultComponent, faultComponentMap, schema } = this.props; + if (faultComponentMap) { + const { componentName } = schema; + return faultComponentMap[componentName] || faultComponent || FaultComponent; + } + return faultComponent || FaultComponent; + } + + getComp() { + const { schema, components } = this.props; + const { componentName } = schema; + const allComponents = { ...RENDERER_COMPS, ...components }; + let Comp = allComponents[componentName] || RENDERER_COMPS[`${componentName}Renderer`]; + if (Comp && Comp.prototype) { + if (!(Comp.prototype instanceof BaseRenderer)) { + Comp = RENDERER_COMPS[`${componentName}Renderer`]; + } + } + return Comp; } render() { @@ -168,24 +142,28 @@ export default function rendererFactory(): IRenderComponent { } // 兼容乐高区块模板 if (schema.componentName !== 'Div' && !isFileSchema(schema)) { + logger.error('The root component name needs to be one of Page、Block、Component, please check the schema: ', schema); return '模型结构异常'; } debug('entry.render'); - const { componentName } = schema; const allComponents = { ...RENDERER_COMPS, ...components }; - let Comp = allComponents[componentName] || RENDERER_COMPS[`${componentName}Renderer`]; - if (Comp && Comp.prototype) { - if (!(Comp.prototype instanceof BaseRenderer)) { - Comp = RENDERER_COMPS[`${componentName}Renderer`]; - } + let Comp = this.getComp(); + + if (this.state && this.state.engineRenderError) { + return createElement(this.getFaultComponent(), { + ...this.props, + error: this.state.error, + }); } if (Comp) { - return createElement(AppContext.Provider, { value: { - appHelper, - components: allComponents, - engine: this, - } }, createElement(ConfigProvider, { + return createElement(AppContext.Provider, { + value: { + appHelper, + components: allComponents, + engine: this, + }, + }, createElement(ConfigProvider, { device: this.props.device, locale: this.props.locale, }, createElement(Comp, { diff --git a/packages/renderer-core/src/renderer/temp.tsx b/packages/renderer-core/src/renderer/temp.tsx index 83adef7e30..1432da5fd2 100644 --- a/packages/renderer-core/src/renderer/temp.tsx +++ b/packages/renderer-core/src/renderer/temp.tsx @@ -1,4 +1,5 @@ import { IBaseRenderComponent } from '../types'; +import logger from '../utils/logger'; import baseRendererFactory from './base'; export default function tempRendererFactory(): IBaseRenderComponent { @@ -41,7 +42,7 @@ export default function tempRendererFactory(): IBaseRenderComponent { } async componentDidCatch(e: any) { - console.warn(e); + logger.warn(e); this.__debug(`componentDidCatch - ${this.props.__schema.fileName}`); } diff --git a/packages/renderer-core/src/types/index.ts b/packages/renderer-core/src/types/index.ts index 2a4c0975e6..afbec272ab 100644 --- a/packages/renderer-core/src/types/index.ts +++ b/packages/renderer-core/src/types/index.ts @@ -1,23 +1,23 @@ import type { ComponentLifecycle, CSSProperties } from 'react'; -import { BuiltinSimulatorHost } from '@alilc/lowcode-designer'; -import { RequestHandler, NodeSchema, NodeData, RootSchema, JSONObject } from '@alilc/lowcode-types'; +import { BuiltinSimulatorHost, BuiltinSimulatorRenderer } from '@alilc/lowcode-designer'; +import { RequestHandler, IPublicTypeNodeSchema, IPublicTypeRootSchema, IPublicTypeJSONObject } from '@alilc/lowcode-types'; -export type ISchema = NodeSchema | RootSchema; +export type ISchema = IPublicTypeNodeSchema | IPublicTypeRootSchema; /* ** Duck typed component type supporting both react and rax */ interface IGeneralComponent<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { + readonly props: Readonly<P> & Readonly<{ children?: any | undefined }>; + state: Readonly<S>; + refs: Record<string, any>; + context: any; setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null), callback?: () => void ): void; forceUpdate(callback?: () => void): void; render(): any; - readonly props: Readonly<P> & Readonly<{ children?: any | undefined }>; - state: Readonly<S>; - refs: Record<string, any>; - context: any; } export type IGeneralConstructor< @@ -60,20 +60,28 @@ export interface ILocationLike { } export type IRendererAppHelper = Partial<{ + /** 全局公共函数 */ utils: Record<string, any>; + /** 全局常量 */ constants: Record<string, any>; + /** react-router 的 location 实例 */ location: ILocationLike; + /** react-router 的 history 实例 */ history: IHistoryLike; + /** @deprecated 已无业务使用 */ match: any; + /** @experimental 内部使用 */ logParams: Record<string, any>; + /** @experimental 内部使用 */ addons: Record<string, any>; + /** @experimental 内部使用 */ requestHandlersMap: Record<string, RequestHandler<{ data: unknown; @@ -86,53 +94,86 @@ export type IRendererAppHelper = Partial<{ * @see @todo @承虎 */ export interface IRendererProps { + /** 符合低代码搭建协议的数据 */ - schema: RootSchema | NodeSchema; + schema: IPublicTypeRootSchema | IPublicTypeNodeSchema; + /** 组件依赖的实例 */ components: Record<string, IGeneralComponent>; + /** CSS 类名 */ className?: string; + /** style */ style?: CSSProperties; + /** id */ id?: string | number; + /** 语言 */ locale?: string; + + /** + * 多语言语料 + * 配置规范参见《低代码搭建组件描述协议》https://lowcode-engine.cn/lowcode 中 2.6 国际化多语言支持 + * */ + messages?: Record<string, any>; + /** 主要用于设置渲染模块的全局上下文,里面定义的内容可以在低代码中通过 this 来访问,比如 this.utils */ appHelper?: IRendererAppHelper; + /** - * 配置规范参见《中后台搭建组件描述协议》,主要在搭建场景中使用,用于提升用户搭建体验。 + * 配置规范参见《低代码搭建组件描述协议》https://lowcode-engine.cn/lowcode + * 主要在搭建场景中使用,用于提升用户搭建体验。 * * > 在生产环境下不需要设置 */ componentsMap?: { [key: string]: any }; + /** 设计模式,可选值:live、design */ designMode?: string; + /** 渲染模块是否挂起,当设置为 true 时,渲染模块最外层容器的 shouldComponentUpdate 将始终返回false,在下钻编辑或者多引擎渲染的场景会用到该参数。 */ suspended?: boolean; + /** 组件获取 ref 时触发的钩子 */ - onCompGetRef?: (schema: NodeSchema, ref: any) => void; + onCompGetRef?: (schema: IPublicTypeNodeSchema, ref: any) => void; + /** 组件 ctx 更新回调 */ - onCompGetCtx?: (schema: NodeSchema, ref: any) => void; + onCompGetCtx?: (schema: IPublicTypeNodeSchema, ref: any) => void; + /** 传入的 schema 是否有变更 */ getSchemaChangedSymbol?: () => boolean; + /** 设置 schema 是否有变更 */ setSchemaChangedSymbol?: (symbol: boolean) => void; + /** 自定义创建 element 的钩子 */ customCreateElement?: (Component: any, props: any, children: any) => any; + /** 渲染类型,标识当前模块是以什么类型进行渲染的 */ rendererName?: 'LowCodeRenderer' | 'PageRenderer' | string; + /** 当找不到组件时,显示的组件 */ notFoundComponent?: IGeneralComponent; + /** 当组件渲染异常时,显示的组件 */ faultComponent?: IGeneralComponent; + + /** */ + faultComponentMap?: { + [prop: string]: IGeneralComponent; + }; + /** 设备信息 */ device?: string; + /** * @default true * JSExpression 是否只支持使用 this 来访问上下文变量 */ thisRequiredInJSE?: boolean; + /** * @default false * 当开启组件未找到严格模式时,渲染模块不会默认给一个容器组件 @@ -154,9 +195,9 @@ export interface IBaseRendererProps { __appHelper: IRendererAppHelper; __components: Record<string, any>; __ctx: Record<string, any>; - __schema: RootSchema; + __schema: IPublicTypeRootSchema; __host?: BuiltinSimulatorHost; - __container?: any; + __container?: BuiltinSimulatorRenderer; config?: Record<string, any>; designMode?: 'design'; className?: string; @@ -167,14 +208,16 @@ export interface IBaseRendererProps { thisRequiredInJSE?: boolean; documentId?: string; getNode?: any; + /** * 设备类型,默认值:'default' */ device?: 'default' | 'mobile' | string; + componentName?: string; } export interface INodeInfo { - schema?: NodeSchema; + schema?: IPublicTypeNodeSchema; Comp: any; componentInfo?: any; componentChildren?: any; @@ -191,7 +234,7 @@ export interface DataSourceItem { type?: string; options?: { uri: string | JSExpression; - params?: JSONObject | JSExpression; + params?: IPublicTypeJSONObject | JSExpression; method?: string | JSExpression; shouldFetch?: string; willFetch?: string; @@ -207,13 +250,13 @@ export interface DataSource { } export interface IRuntime { + [key: string]: any; Component: IGeneralConstructor; PureComponent: IGeneralConstructor; createElement: (...args: any) => any; createContext: (...args: any) => any; forwardRef: (...args: any) => any; findDOMNode: (...args: any) => any; - [key: string]: any; } export interface IRendererModules { @@ -244,7 +287,7 @@ export type IBaseRendererInstance = IGeneralComponent< __beforeInit(props: IBaseRendererProps): void; __init(props: IBaseRendererProps): void; __afterInit(props: IBaseRendererProps): void; - __excuteLifeCycleMethod(method: string, args?: any[]): void; + __executeLifeCycleMethod(method: string, args?: any[]): void; __bindCustomMethods(props: IBaseRendererProps): void; __generateCtx(ctx: Record<string, any>): void; __parseData(data: any, ctx?: any): any; @@ -252,11 +295,11 @@ export type IBaseRendererInstance = IGeneralComponent< __render(): void; __getRef(ref: any): void; __getSchemaChildrenVirtualDom( - schema: NodeSchema | undefined, + schema: IPublicTypeNodeSchema | undefined, Comp: any, nodeChildrenMap?: any ): any; - __getComponentProps(schema: NodeSchema | undefined, scope: any, Comp: any, componentInfo?: any): any; + __getComponentProps(schema: IPublicTypeNodeSchema | undefined, scope: any, Comp: any, componentInfo?: any): any; __createDom(): any; __createVirtualDom(schema: any, self: any, parentInfo: INodeInfo, idx: string | number): any; __createLoopVirtualDom(schema: any, self: any, parentInfo: INodeInfo, idx: number | string): any; @@ -266,7 +309,7 @@ export type IBaseRendererInstance = IGeneralComponent< __renderContextProvider(customProps?: object, children?: any): any; __renderContextConsumer(children: any): any; __renderContent(children: any): any; - __checkSchema(schema: NodeSchema | undefined, extraComponents?: string | string[]): any; + __checkSchema(schema: IPublicTypeNodeSchema | undefined, extraComponents?: string | string[]): any; __renderComp(Comp: any, ctxProps: object): any; $(filedId: string, instance?: any): any; }; @@ -279,21 +322,21 @@ export interface IBaseRenderComponent { } export interface IRenderComponent { + displayName: string; + defaultProps: IRendererProps; + findDOMNode: (...args: any) => any; + new(props: IRendererProps, context: any): IGeneralComponent<IRendererProps, IRendererState> & { [x: string]: any; - componentDidMount(): Promise<void>; - componentDidUpdate(): Promise<void>; - componentWillUnmount(): Promise<void>; - componentDidCatch(e: any): Promise<void>; - shouldComponentUpdate(nextProps: IRendererProps): boolean; __getRef: (ref: any) => void; + componentDidMount(): Promise<void> | void; + componentDidUpdate(): Promise<void> | void; + componentWillUnmount(): Promise<void> | void; + componentDidCatch(e: any): Promise<void> | void; + shouldComponentUpdate(nextProps: IRendererProps): boolean; isValidComponent(SetComponent: any): any; - patchDidCatch(SetComponent: any): void; createElement(SetComponent: any, props: any, children?: any): any; getNotFoundComponent(): any; getFaultComponent(): any; }; - displayName: string; - defaultProps: IRendererProps; - findDOMNode: (...args: any) => any; } diff --git a/packages/renderer-core/src/utils/common.ts b/packages/renderer-core/src/utils/common.ts index d064b5593f..0462d358a7 100644 --- a/packages/renderer-core/src/utils/common.ts +++ b/packages/renderer-core/src/utils/common.ts @@ -1,20 +1,16 @@ /* eslint-disable no-console */ /* eslint-disable no-new-func */ import logger from './logger'; -import { isI18nData, RootSchema, NodeSchema, isJSExpression, JSSlot } from '@alilc/lowcode-types'; +import { IPublicTypeRootSchema, IPublicTypeNodeSchema, IPublicTypeJSSlot } from '@alilc/lowcode-types'; +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', @@ -28,7 +24,7 @@ const EXPRESSION_TYPE = { * @name isSchema * @returns boolean */ -export function isSchema(schema: any): schema is NodeSchema { +export function isSchema(schema: any): schema is IPublicTypeNodeSchema { if (isEmpty(schema)) { return false; } @@ -57,7 +53,7 @@ export function isSchema(schema: any): schema is NodeSchema { * @param schema * @returns boolean */ -export function isFileSchema(schema: NodeSchema): schema is RootSchema { +export function isFileSchema(schema: IPublicTypeNodeSchema): schema is IPublicTypeRootSchema { if (!isSchema(schema)) { return false; } @@ -96,7 +92,7 @@ export function getFileCssName(fileName: string) { * check if a object is type of JSSlot * @returns string */ -export function isJSSlot(obj: any): obj is JSSlot { +export function isJSSlot(obj: any): obj is IPublicTypeJSSlot { if (!obj) { return false; } @@ -156,6 +152,7 @@ export function canAcceptsRef(Comp: any) { // eslint-disable-next-line max-len return Comp?.$$typeof === REACT_FORWARD_REF_TYPE || Comp?.prototype?.isReactComponent || Comp?.prototype?.setState || Comp._forwardRef; } + /** * transform array to a object * @param arr array to be transformed @@ -181,32 +178,6 @@ export function transformArrayToMap(arr: any[], key: string, overwrite = true) { return res; } -export function checkPropTypes(value: any, name: string, rule: any, componentName: string) { - let ruleFunction = rule; - if (typeof rule === 'string') { - ruleFunction = new Function(`"use strict"; const PropTypes = arguments[0]; return ${rule}`)(PropTypes2); - } - if (!ruleFunction || typeof ruleFunction !== 'function') { - console.warn('checkPropTypes should have a function type rule argument'); - return true; - } - const err = ruleFunction( - { - [name]: value, - }, - name, - componentName, - 'prop', - null, - ReactPropTypesSecret, - ); - if (err) { - console.warn(err); - } - return !err; -} - - /** * transform string to a function * @param str function in string form @@ -229,7 +200,26 @@ export function transformStringToFunction(str: string) { * @param self scope object * @returns funtion */ -export function parseExpression(str: any, self: any, thisRequired = false) { + +function parseExpression(options: { + str: any; self: any; thisRequired?: boolean; logScope?: string; +}): any; +function parseExpression(str: any, self: any, thisRequired?: boolean): any; +function parseExpression(a: any, b?: any, c = false) { + let str; + let self; + let thisRequired; + let logScope; + if (typeof a === 'object' && b === undefined) { + str = a.str; + self = a.self; + thisRequired = a.thisRequired; + logScope = a.logScope; + } else { + str = a; + self = b; + thisRequired = c; + } try { const contextArr = ['"use strict";', 'var __self = arguments[0];']; contextArr.push('return '); @@ -249,11 +239,15 @@ export function parseExpression(str: any, self: any, thisRequired = false) { const code = `with(${thisRequired ? '{}' : '$scope || {}'}) { ${tarStr} }`; return new Function('$scope', code)(self); } catch (err) { - logger.error('parseExpression.error', err, str, self?.__self ?? self); + logger.error(`${logScope || ''} parseExpression.error`, err, str, self?.__self ?? self); return undefined; } } +export { + parseExpression, +}; + export function parseThisRequiredExpression(str: any, self: any) { return parseExpression(str, self, true); } @@ -319,11 +313,17 @@ export function forEach(targetObj: any, fn: any, context?: any) { interface IParseOptions { thisRequiredInJSE?: boolean; + logScope?: string; } export function parseData(schema: unknown, self: any, options: IParseOptions = {}): any { if (isJSExpression(schema)) { - return parseExpression(schema, self, options.thisRequiredInJSE); + return parseExpression({ + str: schema, + self, + thisRequired: options.thisRequiredInJSE, + logScope: options.logScope, + }); } else if (isI18nData(schema)) { return parseI18n(schema, self); } else if (typeof schema === 'string') { diff --git a/packages/renderer-core/src/utils/data-helper.ts b/packages/renderer-core/src/utils/data-helper.ts index 9eb152df9e..41bcb9bfa0 100644 --- a/packages/renderer-core/src/utils/data-helper.ts +++ b/packages/renderer-core/src/utils/data-helper.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ /* eslint-disable max-len */ /* eslint-disable object-curly-newline */ -import { isJSFunction } from '@alilc/lowcode-types'; +import { isJSFunction } from '@alilc/lowcode-utils'; import { transformArrayToMap, transformStringToFunction } from './common'; import { jsonp, request, get, post } from './request'; import logger from './logger'; @@ -186,7 +186,7 @@ export class DataHelper { } const { headers, ...otherProps } = otherOptionsObj || {}; if (!req) { - console.warn(`getDataSource API named ${id} not exist`); + logger.warn(`getDataSource API named ${id} not exist`); return; } @@ -215,7 +215,7 @@ export class DataHelper { try { callbackFn && callbackFn(res && res[id]); } catch (e) { - console.error('load请求回调函数报错', e); + logger.error('load请求回调函数报错', e); } return res && res[id]; }) @@ -223,7 +223,7 @@ export class DataHelper { try { callbackFn && callbackFn(null, err); } catch (e) { - console.error('load请求回调函数报错', e); + logger.error('load请求回调函数报错', e); } return err; }); @@ -300,9 +300,9 @@ export class DataHelper { return dataHandlerFun.call(this.host, data, error); } catch (e) { if (id) { - console.error(`[${id}]单个请求数据处理函数运行出错`, e); + logger.error(`[${id}]单个请求数据处理函数运行出错`, e); } else { - console.error('请求数据处理函数运行出错', e); + logger.error('请求数据处理函数运行出错', e); } } } diff --git a/packages/renderer-core/src/utils/is-use-loop.ts b/packages/renderer-core/src/utils/is-use-loop.ts index 913480f638..b6d67a802a 100644 --- a/packages/renderer-core/src/utils/is-use-loop.ts +++ b/packages/renderer-core/src/utils/is-use-loop.ts @@ -1,19 +1,20 @@ -import { isJSExpression, JSExpression } from '@alilc/lowcode-types'; +import { IPublicTypeJSExpression } from '@alilc/lowcode-types'; +import { isJSExpression } from '@alilc/lowcode-utils'; // 1.渲染模式下,loop 是数组,则按照数组长度渲染组件 // 2.设计模式下,loop 需要长度大于 0,按照循环模式渲染,防止无法设计的情况 -export default function isUseLoop(loop: null | any[] | JSExpression, isDesignMode: boolean): boolean { +export default function isUseLoop(loop: null | any[] | IPublicTypeJSExpression, isDesignMode: boolean): boolean { if (isJSExpression(loop)) { return true; } - if (!Array.isArray(loop)) { - return false; - } - if (!isDesignMode) { return true; } + if (!Array.isArray(loop)) { + return false; + } + return loop.length > 0; } diff --git a/packages/renderer-core/src/utils/logger.ts b/packages/renderer-core/src/utils/logger.ts index cf4895bd2e..5b7a276eb6 100644 --- a/packages/renderer-core/src/utils/logger.ts +++ b/packages/renderer-core/src/utils/logger.ts @@ -1,3 +1,3 @@ -import Logger from 'zen-logger'; -// how to use this logger, see https://www.npmjs.com/package/zen-logger +import { Logger } from '@alilc/lowcode-utils'; + export default new Logger({ level: 'warn', bizName: 'renderer' }); \ No newline at end of file diff --git a/packages/renderer-core/tests/adapter/adapter.test.ts b/packages/renderer-core/tests/adapter/adapter.test.ts index a838602fbe..57d92d1d42 100644 --- a/packages/renderer-core/tests/adapter/adapter.test.ts +++ b/packages/renderer-core/tests/adapter/adapter.test.ts @@ -79,15 +79,10 @@ describe('test src/adapter ', () => { }); - it('setEnv/.env/isReact/isRax works', () => { + it('setEnv/.env/isReact works', () => { adapter.setEnv(Env.React); expect(adapter.env).toBe(Env.React); expect(adapter.isReact()).toBeTruthy(); - expect(adapter.isRax()).toBeFalsy(); - adapter.setEnv(Env.Rax); - expect(adapter.env).toBe(Env.Rax); - expect(adapter.isRax()).toBeTruthy(); - expect(adapter.isReact()).toBeFalsy(); }); it('setRenderers/getRenderers works', () => { diff --git a/packages/renderer-core/tests/hoc/leaf.test.tsx b/packages/renderer-core/tests/hoc/leaf.test.tsx index 0e594bc5ab..c21a10be92 100644 --- a/packages/renderer-core/tests/hoc/leaf.test.tsx +++ b/packages/renderer-core/tests/hoc/leaf.test.tsx @@ -35,7 +35,8 @@ const baseRenderer: any = { __container: { rerender: () => { rerenderCount = 1 + rerenderCount; - } + }, + autoRepaintNode: true, }, documentId: '01' }, @@ -82,7 +83,6 @@ beforeEach(() => { }); component = renderer.create( - // @ts-ignore <Div _leaf={DivNode}> <Text _leaf={TextNode} content="content"></Text> </Div> @@ -237,7 +237,6 @@ describe('mini unit render', () => { nodeMap.set(textSchema.id, TextNode); component = renderer.create( - // @ts-ignore <MiniRenderDiv _leaf={MiniRenderDivNode}> <Text _leaf={TextNode} content="content"></Text> </MiniRenderDiv> @@ -284,7 +283,6 @@ describe('mini unit render', () => { nodeMap.set(textSchema.id, TextNode); renderer.create( - // @ts-ignore <div> <Text _leaf={TextNode} content="content"></Text> </div> @@ -308,7 +306,6 @@ describe('mini unit render', () => { }); renderer.create( - // @ts-ignore <div> <Text _leaf={TextNode} content="content"></Text> </div> @@ -323,6 +320,7 @@ describe('mini unit render', () => { it('change component leaf isRoot is true', () => { const TextNode = new Node(textSchema, { isRoot: true, + isRootNode: true, }); nodeMap.set(textSchema.id, TextNode); @@ -355,6 +353,7 @@ describe('mini unit render', () => { id: 'rootId', }, { isRoot: true, + isRootNode: true }), }) }); @@ -385,7 +384,6 @@ describe('mini unit render', () => { }; const component = renderer.create( - // @ts-ignore <MiniRenderDiv _leaf={MiniRenderDivNode}> <Text _leaf={TextNode} content="content"></Text> </MiniRenderDiv> @@ -425,7 +423,6 @@ describe('mini unit render', () => { nodeMap.set(miniRenderSchema.id, MiniRenderDivNode); component = renderer.create( - // @ts-ignore <MiniRenderDiv _leaf={MiniRenderDivNode}> <Text _leaf={TextNode} content="content"></Text> </MiniRenderDiv> @@ -488,7 +485,6 @@ describe('onVisibleChange', () => { describe('children', () => { it('this.props.children is array', () => { const component = renderer.create( - // @ts-ignore <Div _leaf={DivNode}> <Text _leaf={TextNode} content="content"></Text> <Text _leaf={TextNode} content="content"></Text> @@ -508,6 +504,41 @@ describe('onChildrenChange', () => { DivNode.emitChildrenChange(); makeSnapshot(component); }); + + it('children is 0', () => { + DivNode.schema.children = 0 + DivNode.emitChildrenChange(); + const componentInstance = component.root; + expect(componentInstance.findByType(components.Div).props.children).toEqual(0); + }); + + it('children is false', () => { + DivNode.schema.children = false + DivNode.emitChildrenChange(); + const componentInstance = component.root; + expect(componentInstance.findByType(components.Div).props.children).toEqual(false); + }); + + it('children is []', () => { + DivNode.schema.children = [] + DivNode.emitChildrenChange(); + const componentInstance = component.root; + expect(componentInstance.findByType(components.Div).props.children).toEqual([]); + }); + + it('children is null', () => { + DivNode.schema.children = null + DivNode.emitChildrenChange(); + const componentInstance = component.root; + expect(componentInstance.findByType(components.Div).props.children).toEqual(null); + }); + + it('children is undefined', () => { + DivNode.schema.children = undefined; + DivNode.emitChildrenChange(); + const componentInstance = component.root; + expect(componentInstance.findByType(components.Div).props.children).toEqual(undefined); + }); }); describe('not render leaf', () => { diff --git a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap index 169ed545c6..79c5f0f085 100644 --- a/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap +++ b/packages/renderer-core/tests/renderer/__snapshots__/renderer.test.tsx.snap @@ -11,6 +11,12 @@ exports[`Base Render renderComp 1`] = ` > <div __id="node_dockcy8n9xed" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -25,6 +31,12 @@ exports[`Base Render renderComp 1`] = ` > <div __id="node_dockcy8n9xee" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -39,6 +51,12 @@ exports[`Base Render renderComp 1`] = ` > <nav __id="node_dockcy8n9xef" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } aria-label="Breadcrumb" style={ Object { @@ -55,6 +73,12 @@ exports[`Base Render renderComp 1`] = ` > <span __id="node_dockcy8n9xeg" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-breadcrumb-text" > 首页 @@ -74,6 +98,12 @@ exports[`Base Render renderComp 1`] = ` > <span __id="node_dockcy8n9xei" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-breadcrumb-text" > 品质中台 @@ -93,6 +123,12 @@ exports[`Base Render renderComp 1`] = ` > <span __id="node_dockcy8n9xek" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-breadcrumb-text" > 商家品质页面管理 @@ -112,6 +148,12 @@ exports[`Base Render renderComp 1`] = ` > <span __id="node_dockcy8n9xem" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } aria-current="page" className="next-breadcrumb-text activated" > @@ -123,6 +165,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockcy8n9xeo" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -138,6 +186,12 @@ exports[`Base Render renderComp 1`] = ` <form __events={Array []} __id="node_dockcy8n9xep" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-form next-inline next-medium" onSubmit={[Function]} role="grid" @@ -151,6 +205,12 @@ exports[`Base Render renderComp 1`] = ` > <div __id="node_dockcy8n9xeq" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-form-item next-left next-medium" style={ Object { @@ -193,6 +253,12 @@ exports[`Base Render renderComp 1`] = ` > <input __id="node_dockcy8n9xer" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } autoComplete="off" disabled={false} height="100%" @@ -258,6 +324,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockcy8n9xes" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-form-item next-left next-medium" style={ Object { @@ -300,6 +372,12 @@ exports[`Base Render renderComp 1`] = ` > <input __id="node_dockcy8n9xet" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } autoComplete="off" disabled={false} height="100%" @@ -365,6 +443,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockcy8n9xeu" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-form-item next-left next-medium" style={ Object { @@ -392,6 +476,12 @@ exports[`Base Render renderComp 1`] = ` > <input __id="node_dockcy8n9xev" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } autoComplete="off" disabled={false} height="100%" @@ -412,10 +502,22 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockcy8n9xew" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-btn-group" > <button __id="node_dockcy8n9xex" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-btn next-medium next-btn-primary" disabled={false} onClick={[Function]} @@ -435,6 +537,12 @@ exports[`Base Render renderComp 1`] = ` </button> <button __id="node_dockcy8n9xe10" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-btn next-medium next-btn-normal" disabled={false} onClick={[Function]} @@ -457,6 +565,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockcy8n9xe1f" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -482,6 +596,12 @@ exports[`Base Render renderComp 1`] = ` ] } __id="node_dockd5nrh9p4" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-btn next-medium next-btn-primary" disabled={false} onClick={[Function]} @@ -498,6 +618,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockd5nrh9p5" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -510,6 +636,12 @@ exports[`Base Render renderComp 1`] = ` > <div __id="node_dockjielosj1" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } actionBar={ Array [ Object { @@ -688,6 +820,12 @@ exports[`Base Render renderComp 1`] = ` </div> <div __id="node_dockd5nrh9pg" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-box" style={ Object { @@ -702,6 +840,12 @@ exports[`Base Render renderComp 1`] = ` > <div __id="node_dockd5nrh9pf" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="next-pagination next-medium next-normal" style={Object {}} > @@ -898,6 +1042,12 @@ exports[`Base Render renderComp 1`] = ` > <input __id="node_dockd5nrh9pr" + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } autoComplete="off" disabled={false} height="100%" @@ -967,6 +1117,12 @@ exports[`JSExpression JSExpression props 1`] = ` style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} visible={true} @@ -980,12 +1136,24 @@ exports[`JSExpression JSExpression props with loop 1`] = ` style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} name1="1" name2="1" /> <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} name1="2" @@ -1000,11 +1168,23 @@ exports[`JSExpression JSExpression props with loop, and thisRequiredInJSE is tru style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} name1="1" /> <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} name1="2" @@ -1018,6 +1198,12 @@ exports[`JSExpression JSFunction props 1`] = ` style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} onClick={[Function]} @@ -1032,6 +1218,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` > <div __id="node_ocl1ao1o7w3" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { padding: 12px; background: #f2f2f2; @@ -1046,6 +1238,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` > <div __id="node_ocl1ao1o7w4" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { font-size: 14px; color: #666; @@ -1063,6 +1261,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` </div> <div __id="node_ocl1ao1o7w3" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { padding: 12px; background: #f2f2f2; @@ -1077,6 +1281,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` > <div __id="node_ocl1ao1o7w4" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { font-size: 14px; color: #666; @@ -1094,6 +1304,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` </div> <div __id="node_ocl1ao1o7w3" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { padding: 12px; background: #f2f2f2; @@ -1108,6 +1324,12 @@ exports[`JSExpression JSSlot has loop 1`] = ` > <div __id="node_ocl1ao1o7w4" + __inner__={ + Object { + "condition": true, + "hidden": false, + } + } __style__=":root { font-size: 14px; color: #666; @@ -1132,6 +1354,12 @@ exports[`JSExpression base props 1`] = ` style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} text="123" @@ -1146,10 +1374,22 @@ exports[`designMode designMode:default 1`] = ` style={Object {}} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut" forwardRef={[Function]} > <div + __inner__={ + Object { + "condition": true, + "hidden": undefined, + } + } className="div-ut-children" forwardRef={[Function]} /> diff --git a/packages/renderer-core/tests/renderer/base.test.tsx b/packages/renderer-core/tests/renderer/base.test.tsx index fd9b453451..3faa2bcf44 100644 --- a/packages/renderer-core/tests/renderer/base.test.tsx +++ b/packages/renderer-core/tests/renderer/base.test.tsx @@ -1,5 +1,5 @@ -import React, { Component, createElement, PureComponent, createContext } from 'react'; +import React, { Component, createElement, forwardRef, PureComponent, createContext } from 'react'; const mockGetRenderers = jest.fn(); const mockGetRuntime = jest.fn(); const mockParseExpression = jest.fn(); @@ -59,6 +59,7 @@ describe('Base Render methods', () => { createElement, PureComponent, createContext, + forwardRef, }); RendererClass = baseRendererFactory(); }) @@ -78,7 +79,6 @@ describe('Base Render methods', () => { // const originalUtils = jest.requireActual('../../src/utils'); // mockParseExpression.mockImplementation(originalUtils.parseExpression); const component = TestRenderer.create( - // @ts-ignore <RendererClass __schema={mockSchema} components={components as any} @@ -120,7 +120,7 @@ describe('Base Render methods', () => { // it('should excute lifecycle.componentDidCatch when defined', () => { // }); - // it('__excuteLifeCycleMethod should work', () => { + // it('__executeLifeCycleMethod should work', () => { // }); // it('reloadDataSource should work', () => { diff --git a/packages/renderer-core/tests/renderer/renderer.test.tsx b/packages/renderer-core/tests/renderer/renderer.test.tsx index 36ee4167e8..081cede8ab 100644 --- a/packages/renderer-core/tests/renderer/renderer.test.tsx +++ b/packages/renderer-core/tests/renderer/renderer.test.tsx @@ -13,7 +13,6 @@ function getComp(schema, comp = null, others = {}): Promise<{ }> { return new Promise((resolve, reject) => { const component = renderer.create( - // @ts-ignore <Renderer schema={schema} components={components as any} @@ -48,7 +47,6 @@ afterEach(() => { describe('Base Render', () => { it('renderComp', () => { const content = ( - // @ts-ignore <Renderer schema={schema as any} components={components as any} diff --git a/packages/renderer-core/tests/setup.ts b/packages/renderer-core/tests/setup.ts index fd6fd9e5df..0d51f6bb5a 100644 --- a/packages/renderer-core/tests/setup.ts +++ b/packages/renderer-core/tests/setup.ts @@ -1,23 +1,10 @@ -jest.mock('zen-logger', () => { - class Logger { - log() {} - error() {} - warn() {} - debug() {} - } - return { - __esModule: true, - default: Logger, - }; -}); - jest.mock('lodash', () => { const original = jest.requireActual('lodash'); return { ...original, - debounce: (fn) => () => fn(), - throttle: (fn) => () => fn(), + debounce: (fn) => (...args: any[]) => fn.apply(this, args), + throttle: (fn) => (...args: any[]) => fn.apply(this, args), } }) diff --git a/packages/renderer-core/tests/utils/common.test.ts b/packages/renderer-core/tests/utils/common.test.ts index 6fac55024f..13b6908d50 100644 --- a/packages/renderer-core/tests/utils/common.test.ts +++ b/packages/renderer-core/tests/utils/common.test.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { isSchema, isFileSchema, @@ -278,7 +277,7 @@ describe('test capitalizeFirstLetter ', () => { describe('test forEach ', () => { it('should work', () => { const mockFn = jest.fn(); - + forEach(null, mockFn); expect(mockFn).toBeCalledTimes(0); @@ -293,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'); @@ -374,7 +373,7 @@ describe('test parseThisRequiredExpression', () => { }; const fn = logger.error = jest.fn(); parseThisRequiredExpression(mockExpression, { state: { text: 'text' } }); - expect(fn).toBeCalledWith('parseExpression.error', new ReferenceError('state is not defined'), {"type": "JSExpression", "value": "state.text"}, {"state": {"text": "text"}}); + expect(fn).toBeCalledWith(' parseExpression.error', new ReferenceError('state is not defined'), {"type": "JSExpression", "value": "state.text"}, {"state": {"text": "text"}}); }); it('[success] JSExpression handle without this use scopeValue', () => { @@ -461,4 +460,4 @@ describe('test parseData ', () => { expect(result.__privateKey).toBeUndefined(); }); -}); \ No newline at end of file +}); diff --git a/packages/renderer-core/tests/utils/data-helper.test.ts b/packages/renderer-core/tests/utils/data-helper.test.ts index cd4508ea87..f4b388ce92 100644 --- a/packages/renderer-core/tests/utils/data-helper.test.ts +++ b/packages/renderer-core/tests/utils/data-helper.test.ts @@ -346,11 +346,6 @@ describe('test DataHelper ', () => { result = dataHelper.handleData('fullConfigGet', mockDataHandler, { data: 'mockDataValue' }, null); expect(result).toStrictEqual({ data: 'mockDataValue' }); - // test exception - const mockError = jest.fn(); - const orginalConsole = global.console; - global.console = { error: mockError }; - // exception with id mockDataHandler = { type: 'JSFunction', @@ -358,7 +353,6 @@ describe('test DataHelper ', () => { }; result = dataHelper.handleData('fullConfigGet', mockDataHandler, { data: 'mockDataValue' }, null); expect(result).toBeUndefined(); - expect(mockError).toBeCalledWith('[fullConfigGet]单个请求数据处理函数运行出错', expect.anything()); // exception without id mockDataHandler = { @@ -367,12 +361,8 @@ describe('test DataHelper ', () => { }; result = dataHelper.handleData(null, mockDataHandler, { data: 'mockDataValue' }, null); expect(result).toBeUndefined(); - expect(mockError).toBeCalledWith('请求数据处理函数运行出错', expect.anything()); - - global.console = orginalConsole; }); - it('updateConfig should work', () => { const mockHost = { stateA: 'aValue'}; const mockDataSourceConfig = { diff --git a/packages/renderer-core/tests/utils/is-use-loop.test.ts b/packages/renderer-core/tests/utils/is-use-loop.test.ts index 5f502a2e5b..b0a614f2ee 100644 --- a/packages/renderer-core/tests/utils/is-use-loop.test.ts +++ b/packages/renderer-core/tests/utils/is-use-loop.test.ts @@ -5,6 +5,9 @@ describe('base test', () => { it('designMode is true', () => { expect(isUseLoop([], true)).toBeFalsy(); expect(isUseLoop([{}], true)).toBeTruthy(); + expect(isUseLoop(null, true)).toBeFalsy(); + expect(isUseLoop(undefined, true)).toBeFalsy(); + expect(isUseLoop(0, true)).toBeFalsy(); }); it('loop is expression', () => { @@ -21,5 +24,8 @@ describe('base test', () => { it('designMode is false', () => { expect(isUseLoop([], false)).toBeTruthy(); expect(isUseLoop([{}], false)).toBeTruthy(); + expect(isUseLoop(null, false)).toBeTruthy(); + expect(isUseLoop(undefined, false)).toBeTruthy(); + expect(isUseLoop(0, false)).toBeTruthy(); }); }); diff --git a/packages/renderer-core/tests/utils/node.ts b/packages/renderer-core/tests/utils/node.ts index 01da07a695..01c6ab507c 100644 --- a/packages/renderer-core/tests/utils/node.ts +++ b/packages/renderer-core/tests/utils/node.ts @@ -1,4 +1,4 @@ -import { PropChangeOptions } from "@ali/lowcode-designer"; +import { IPublicTypePropChangeOptions } from "@ali/lowcode-designer"; import EventEmitter from "events"; export default class Node { @@ -40,6 +40,10 @@ export default class Node { isRoot = () => this._isRoot; + get isRootNode () { + return this._isRoot; + }; + // componentMeta() { // return this.componentMeta; // } @@ -66,7 +70,7 @@ export default class Node { } } - emitPropChange(val: PropChangeOptions, skip?: boolean) { + emitPropChange(val: IPublicTypePropChangeOptions, skip?: boolean) { if (!skip) { this.schema.props = { ...this.schema.props, diff --git a/packages/shell/build.json b/packages/shell/build.json index bd5cf18dde..3e92600554 100644 --- a/packages/shell/build.json +++ b/packages/shell/build.json @@ -1,5 +1,5 @@ { "plugins": [ - "build-plugin-component" + "@alilc/build-plugin-lce" ] } diff --git a/packages/shell/build.test.json b/packages/shell/build.test.json index dcdc891e93..9cc30d7463 100644 --- a/packages/shell/build.test.json +++ b/packages/shell/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ] } diff --git a/packages/shell/package.json b/packages/shell/package.json index 8af37b3759..c2b62e2270 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-shell", - "version": "1.0.15", + "version": "1.3.2", "description": "Shell Layer for AliLowCodeEngine", "main": "lib/index.js", "module": "es/index.js", @@ -9,27 +9,24 @@ "es" ], "scripts": { - "build": "build-scripts build --skip-demo", - "test": "build-scripts test --config build.test.json", - "test:cov": "build-scripts test --config build.test.json --jest-coverage" + "build": "build-scripts build" }, "license": "MIT", "dependencies": { - "@alilc/lowcode-designer": "1.0.15", - "@alilc/lowcode-editor-core": "1.0.15", - "@alilc/lowcode-editor-skeleton": "1.0.15", - "@alilc/lowcode-types": "1.0.15", - "@alilc/lowcode-utils": "1.0.15", + "@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", "react": "^16", - "react-dom": "^16.7.0", - "zen-logger": "^1.1.0" + "react-dom": "^16.7.0" }, "devDependencies": { "@alib/build-scripts": "^0.1.29", - "@alilc/lowcode-test-mate": "^1.0.1", "@testing-library/react": "^11.2.2", "@types/classnames": "^2.2.7", "@types/jest": "^26.0.16", @@ -38,9 +35,6 @@ "@types/node": "^13.7.1", "@types/react": "^16", "@types/react-dom": "^16", - "babel-jest": "^26.5.2", - "build-plugin-component": "^0.2.10", - "build-scripts-config": "^0.1.8", "jest": "^26.6.3", "lodash": "^4.17.20", "moment": "^2.29.1", @@ -50,12 +44,11 @@ "access": "public", "registry": "https://registry.npmjs.org/" }, - "resolutions": { - "@builder/babel-preset-ice": "1.0.1" - }, "repository": { "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/shell" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/shell/src/api/canvas.ts b/packages/shell/src/api/canvas.ts new file mode 100644 index 0000000000..48acbc487a --- /dev/null +++ b/packages/shell/src/api/canvas.ts @@ -0,0 +1,82 @@ +import { + IPublicApiCanvas, + IPublicModelDropLocation, + IPublicModelScrollTarget, + IPublicTypeScrollable, + IPublicModelScroller, + IPublicTypeLocationData, + IPublicModelEditor, + IPublicModelDragon, + IPublicModelActiveTracker, + IPublicModelClipboard, +} from '@alilc/lowcode-types'; +import { + ScrollTarget as InnerScrollTarget, + IDesigner, +} from '@alilc/lowcode-designer'; +import { editorSymbol, designerSymbol, nodeSymbol } from '../symbols'; +import { + Dragon as ShellDragon, + DropLocation as ShellDropLocation, + ActiveTracker as ShellActiveTracker, + Clipboard as ShellClipboard, + DropLocation, +} from '../model'; + +const clipboardInstanceSymbol = Symbol('clipboardInstace'); + +export class Canvas implements IPublicApiCanvas { + private readonly [editorSymbol]: IPublicModelEditor; + private readonly [clipboardInstanceSymbol]: IPublicModelClipboard; + + private get [designerSymbol](): IDesigner { + return this[editorSymbol].get('designer') as IDesigner; + } + + get dragon(): IPublicModelDragon | null { + return ShellDragon.create(this[designerSymbol].dragon, this.workspaceMode); + } + + get activeTracker(): IPublicModelActiveTracker | null { + const activeTracker = new ShellActiveTracker(this[designerSymbol].activeTracker); + return activeTracker; + } + + get isInLiveEditing(): boolean { + return Boolean(this[editorSymbol].get('designer')?.project?.simulator?.liveEditing?.editing); + } + + get clipboard(): IPublicModelClipboard { + return this[clipboardInstanceSymbol]; + } + + constructor(editor: IPublicModelEditor, readonly workspaceMode: boolean = false) { + this[editorSymbol] = editor; + this[clipboardInstanceSymbol] = new ShellClipboard(); + } + + createScrollTarget(shell: HTMLDivElement): IPublicModelScrollTarget { + return new InnerScrollTarget(shell); + } + + createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller { + return this[designerSymbol].createScroller(scrollable); + } + + /** + * 创建插入位置,考虑放到 dragon 中 + */ + createLocation(locationData: IPublicTypeLocationData): IPublicModelDropLocation { + return new DropLocation(this[designerSymbol].createLocation({ + ...locationData, + target: (locationData.target as any)[nodeSymbol], + })); + } + + /** + * @deprecated + */ + get dropLocation() { + return ShellDropLocation.create((this[designerSymbol] as any).dropLocation || null); + } +} 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<string>(); + +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/common.tsx b/packages/shell/src/api/common.tsx new file mode 100644 index 0000000000..8ce07153ad --- /dev/null +++ b/packages/shell/src/api/common.tsx @@ -0,0 +1,468 @@ +import { editorSymbol, skeletonSymbol, designerCabinSymbol, designerSymbol, settingFieldSymbol, editorCabinSymbol, skeletonCabinSymbol } from '../symbols'; +import { + isFormEvent as innerIsFormEvent, + compatibleLegaoSchema as innerCompatibleLegaoSchema, + getNodeSchemaById as innerGetNodeSchemaById, + transactionManager, + isNodeSchema as innerIsNodeSchema, + isDragNodeDataObject as innerIsDragNodeDataObject, + isDragNodeObject as innerIsDragNodeObject, + isDragAnyObject as innerIsDragAnyObject, + isLocationChildrenDetail as innerIsLocationChildrenDetail, + isNode as innerIsNode, + isSettingField, + isSettingField as innerIsSettingField, +} from '@alilc/lowcode-utils'; +import { + IPublicTypeNodeSchema, + IPublicEnumTransitionType, + IPublicEnumTransformStage as InnerTransitionStage, + IPublicApiCommonDesignerCabin, + IPublicApiCommonSkeletonCabin, + IPublicApiCommonUtils, + IPublicApiCommon, + IPublicEnumDragObjectType as InnerDragObjectType, + IPublicTypeLocationDetailType as InnerLocationDetailType, + IPublicApiCommonEditorCabin, + IPublicModelDragon, + IPublicModelSettingField, + IPublicTypeI18nData, +} from '@alilc/lowcode-types'; +import { + SettingField as InnerSettingField, + LiveEditing as InnerLiveEditing, + isShaken as innerIsShaken, + contains as innerContains, + ScrollTarget as InnerScrollTarget, + getConvertedExtraKey as innerGetConvertedExtraKey, + getOriginalExtraKey as innerGetOriginalExtraKey, + IDesigner, + DropLocation as InnerDropLocation, + Designer as InnerDesigner, + Node as InnerNode, + LowCodePluginManager as InnerLowCodePluginManager, + DesignerView as InnerDesignerView, +} from '@alilc/lowcode-designer'; +import { + Skeleton as InnerSkeleton, + createSettingFieldView as innerCreateSettingFieldView, + PopupContext as InnerPopupContext, + PopupPipe as InnerPopupPipe, + Workbench as InnerWorkbench, + SettingsPrimaryPane as InnerSettingsPrimaryPane, + registerDefaults as InnerRegisterDefaults, +} from '@alilc/lowcode-editor-skeleton'; +import { + Editor, + Title as InnerTitle, + Tip as InnerTip, + shallowIntl as innerShallowIntl, + createIntl as innerCreateIntl, + intl as innerIntl, + globalLocale as innerGlobalLocale, + obx as innerObx, + observable as innerObservable, + makeObservable as innerMakeObservable, + untracked as innerUntracked, + computed as innerComputed, + observer as innerObserver, + action as innerAction, + runInAction as innerRunInAction, + engineConfig as innerEngineConfig, + globalContext, +} from '@alilc/lowcode-editor-core'; +import { Dragon as ShellDragon } from '../model'; +import { ReactNode } from 'react'; + +class DesignerCabin implements IPublicApiCommonDesignerCabin { + private readonly [editorSymbol]: Editor; + + /** + * @deprecated + */ + readonly [designerCabinSymbol]: any; + + private get [designerSymbol](): IDesigner { + return this[editorSymbol].get('designer') as IDesigner; + } + + constructor(editor: Editor) { + this[editorSymbol] = editor; + this[designerCabinSymbol] = { + isDragNodeObject: innerIsDragNodeObject, + isDragAnyObject: innerIsDragAnyObject, + isShaken: innerIsShaken, + contains: innerContains, + LocationDetailType: InnerLocationDetailType, + isLocationChildrenDetail: innerIsLocationChildrenDetail, + ScrollTarget: InnerScrollTarget, + isSettingField: innerIsSettingField, + TransformStage: InnerTransitionStage, + SettingField: InnerSettingField, + LiveEditing: InnerLiveEditing, + DragObjectType: InnerDragObjectType, + isDragNodeDataObject: innerIsDragNodeDataObject, + isNode: innerIsNode, + DropLocation: InnerDropLocation, + Designer: InnerDesigner, + Node: InnerNode, + LowCodePluginManager: InnerLowCodePluginManager, + DesignerView: InnerDesignerView, + }; + } + + /** + * 是否是 SettingField 实例 + * @deprecated use same function from @alilc/lowcode-utils directly + */ + isSettingField(obj: any): boolean { + return isSettingField(obj); + } + + /** + * 转换类型枚举对象,包含 init / upgrade / render 等类型 + * [参考](https://github.com/alibaba/lowcode-engine/blob/main/packages/types/src/transform-stage.ts) + * @deprecated use { TransformStage } from '@alilc/lowcode-types' instead + */ + get TransformStage() { + return InnerTransitionStage; + } + + /** + * @deprecated + */ + get SettingField() { + return InnerSettingField; + } + + /** + * @deprecated + */ + get LiveEditing() { + return InnerLiveEditing; + } + + /** + * @deprecated + */ + get DragObjectType() { + return InnerDragObjectType; + } + + /** + * @deprecated + */ + isDragNodeDataObject(obj: any): boolean { + return innerIsDragNodeDataObject(obj); + } + + /** + * @deprecated + */ + isNode(node: any): boolean { + return innerIsNode(node); + } + + /** + * @deprecated please use canvas.dragon + */ + get dragon(): IPublicModelDragon | null { + return ShellDragon.create(this[designerSymbol].dragon, false); + } +} + +class SkeletonCabin implements IPublicApiCommonSkeletonCabin { + private readonly [skeletonSymbol]: InnerSkeleton; + + readonly [skeletonCabinSymbol]: any; + + constructor(skeleton: InnerSkeleton) { + this[skeletonSymbol] = skeleton; + this[skeletonCabinSymbol] = { + Workbench: InnerWorkbench, + createSettingFieldView: this.createSettingFieldView, + PopupContext: InnerPopupContext, + PopupPipe: InnerPopupPipe, + SettingsPrimaryPane: InnerSettingsPrimaryPane, + registerDefaults: InnerRegisterDefaults, + Skeleton: InnerSkeleton, + }; + } + + get Workbench(): any { + const innerSkeleton = this[skeletonSymbol]; + return (props: any) => <InnerWorkbench {...props} skeleton={innerSkeleton} />; + } + + /** + * @deprecated + */ + createSettingFieldView(field: IPublicModelSettingField, fieldEntry: any) { + return innerCreateSettingFieldView((field as any)[settingFieldSymbol] || field, fieldEntry); + } + + /** + * @deprecated + */ + get PopupContext(): any { + return InnerPopupContext; + } + + /** + * @deprecated + */ + get PopupPipe(): any { + return InnerPopupPipe; + } +} + +class Utils implements IPublicApiCommonUtils { + isNodeSchema(data: any): data is IPublicTypeNodeSchema { + return innerIsNodeSchema(data); + } + + isFormEvent(e: KeyboardEvent | MouseEvent): boolean { + return innerIsFormEvent(e); + } + + /** + * @deprecated this is a legacy api, do not use this if not using is already + */ + compatibleLegaoSchema(props: any): any { + return innerCompatibleLegaoSchema(props); + } + + getNodeSchemaById( + schema: IPublicTypeNodeSchema, + nodeId: string, + ): IPublicTypeNodeSchema | undefined { + return innerGetNodeSchemaById(schema, nodeId); + } + + getConvertedExtraKey(key: string): string { + return innerGetConvertedExtraKey(key); + } + + getOriginalExtraKey(key: string): string { + return innerGetOriginalExtraKey(key); + } + + executeTransaction( + fn: () => void, + type: IPublicEnumTransitionType = IPublicEnumTransitionType.REPAINT, + ): void { + transactionManager.executeTransaction(fn, type); + } + + createIntl(instance: string | object): { + intlNode(id: string, params?: object): ReactNode; + intl(id: string, params?: object): string; + getLocale(): string; + setLocale(locale: string): void; + } { + return innerCreateIntl(instance); + } + + intl(data: IPublicTypeI18nData | string, params?: object): any { + return innerIntl(data, params); + } +} + +class EditorCabin implements IPublicApiCommonEditorCabin { + private readonly [editorSymbol]: Editor; + + /** + * @deprecated + */ + readonly [editorCabinSymbol]: any; + + constructor(editor: Editor) { + this[editorSymbol] = editor; + this[editorCabinSymbol] = { + Editor, + globalContext, + runInAction: innerRunInAction, + Title: InnerTitle, + Tip: InnerTip, + shallowIntl: innerShallowIntl, + createIntl: innerCreateIntl, + intl: innerIntl, + createSetterContent: this.createSetterContent.bind(this), + globalLocale: innerGlobalLocale, + obx: innerObx, + action: innerAction, + engineConfig: innerEngineConfig, + observable: innerObservable, + makeObservable: innerMakeObservable, + untracked: innerUntracked, + computed: innerComputed, + observer: innerObserver, + }; + } + + /** + * Title 组件 + * @experimental unstable API, pay extra caution when trying to use this + */ + get Title() { + return InnerTitle; + } + + /** + * Tip 组件 + * @experimental unstable API, pay extra caution when trying to use this + */ + get Tip() { + return InnerTip; + } + + /** + * @deprecated + */ + shallowIntl(data: any): any { + return innerShallowIntl(data); + } + + /** + * @deprecated use common.utils.createIntl instead + */ + createIntl(instance: any): any { + return innerCreateIntl(instance); + } + + /** + * @deprecated + */ + intl(data: any, params?: object): any { + return innerIntl(data, params); + } + + /** + * @deprecated + */ + createSetterContent = (setter: any, props: Record<string, any>): ReactNode => { + const setters = this[editorSymbol].get('setters'); + return setters.createSetterContent(setter, props); + }; + + /** + * @deprecated use common.utils.createIntl instead + */ + get globalLocale(): any { + return innerGlobalLocale; + } + + /** + * @deprecated + */ + get obx() { + return innerObx; + } + + /** + * @deprecated + */ + get action() { + return innerAction; + } + + /** + * @deprecated + */ + get engineConfig() { + return innerEngineConfig; + } + + /** + * @deprecated + */ + get runInAction() { + return innerRunInAction; + } + + /** + * @deprecated + */ + get observable() { + return innerObservable; + } + + /** + * @deprecated + */ + makeObservable(target: any, annotations: any, options: any) { + return innerMakeObservable(target, annotations, options); + } + + /** + * @deprecated + */ + untracked(action: any) { + return innerUntracked(action); + } + + /** + * @deprecated + */ + get computed() { + return innerComputed; + } + + /** + * @deprecated + */ + observer(component: any) { + return innerObserver(component); + } +} + +export class Common implements IPublicApiCommon { + private readonly __designerCabin: any; + private readonly __skeletonCabin: any; + private readonly __editorCabin: any; + private readonly __utils: Utils; + + constructor(editor: Editor, skeleton: InnerSkeleton) { + this.__designerCabin = new DesignerCabin(editor); + this.__skeletonCabin = new SkeletonCabin(skeleton); + this.__editorCabin = new EditorCabin(editor); + this.__utils = new Utils(); + } + + get utils(): any { + return this.__utils; + } + + /** + * 历史原因导致此处设计不合理,慎用。 + * this load of crap will be removed in some future versions, don`t use it. + * @deprecated + */ + get editorCabin(): any { + return this.__editorCabin; + } + + /** + * 历史原因导致此处设计不合理,慎用。 + * this load of crap will be removed in some future versions, don`t use it. + * @deprecated use canvas api instead + */ + get designerCabin(): any { + return this.__designerCabin; + } + + get skeletonCabin(): any { + return this.__skeletonCabin; + } + + /** + * 历史原因导致此处设计不合理,慎用。 + * this load of crap will be removed in some future versions, don`t use it. + * @deprecated use { TransformStage } from '@alilc/lowcode-types' instead + */ + get objects(): any { + return { + TransformStage: InnerTransitionStage, + }; + } +} \ No newline at end of file diff --git a/packages/shell/src/api/commonUI.tsx b/packages/shell/src/api/commonUI.tsx new file mode 100644 index 0000000000..69dd104b2a --- /dev/null +++ b/packages/shell/src/api/commonUI.tsx @@ -0,0 +1,78 @@ +import { IPublicApiCommonUI, IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; +import { + HelpTip, + IEditor, + Tip as InnerTip, + Title as InnerTitle, + } from '@alilc/lowcode-editor-core'; +import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next'; +import { ContextMenu } from '../components/context-menu'; +import { editorSymbol } from '../symbols'; +import { ReactElement } from 'react'; + +export class CommonUI implements IPublicApiCommonUI { + [editorSymbol]: IEditor; + + Balloon = Balloon; + Breadcrumb = Breadcrumb; + Button = Button; + Card = Card; + Checkbox = Checkbox; + DatePicker = DatePicker; + Dialog = Dialog; + Dropdown = Dropdown; + Form = Form; + Icon = Icon; + Input = Input; + Loading = Loading as any; + Message = Message; + Overlay = Overlay; + Pagination = Pagination; + Radio = Radio; + Search = Search; + Select = Select; + SplitButton = SplitButton; + Step = Step; + Switch = Switch; + Tab = Tab; + Table = Table; + Tree = Tree; + TreeSelect = TreeSelect; + Upload = Upload; + Divider = Divider; + + ContextMenu: ((props: { + menus: IPublicTypeContextMenuAction[]; + children: React.ReactElement[] | React.ReactElement; + }) => ReactElement) & { + create(menus: IPublicTypeContextMenuAction[], event: MouseEvent | React.MouseEvent): void; + }; + + constructor(editor: IEditor) { + this[editorSymbol] = editor; + + const innerContextMenu = (props: any) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return <ContextMenu {...props} pluginContext={pluginContext} />; + }; + + innerContextMenu.create = (menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + const pluginContext: IPublicModelPluginContext = editor.get('pluginContext') as IPublicModelPluginContext; + return ContextMenu.create(pluginContext, menus, event); + }; + + this.ContextMenu = innerContextMenu; + } + + get Tip() { + return InnerTip; + } + + get HelpTip() { + return HelpTip; + } + + get Title() { + return InnerTitle; + } +} diff --git a/packages/shell/src/api/config.ts b/packages/shell/src/api/config.ts new file mode 100644 index 0000000000..d841208780 --- /dev/null +++ b/packages/shell/src/api/config.ts @@ -0,0 +1,39 @@ +import { IPublicModelEngineConfig, IPublicModelPreference, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { configSymbol } from '../symbols'; +import { IEngineConfig } from '@alilc/lowcode-editor-core'; + +export class Config implements IPublicModelEngineConfig { + private readonly [configSymbol]: IEngineConfig; + + constructor(innerEngineConfig: IEngineConfig) { + this[configSymbol] = innerEngineConfig; + } + + has(key: string): boolean { + return this[configSymbol].has(key); + } + + get(key: string, defaultValue?: any): any { + return this[configSymbol].get(key, defaultValue); + } + + set(key: string, value: any): void { + this[configSymbol].set(key, value); + } + + setConfig(config: { [key: string]: any }): void { + this[configSymbol].setConfig(config); + } + + onceGot(key: string): Promise<any> { + return this[configSymbol].onceGot(key); + } + + onGot(key: string, fn: (data: any) => void): IPublicTypeDisposable { + return this[configSymbol].onGot(key, fn); + } + + getPreference(): IPublicModelPreference { + return this[configSymbol].getPreference(); + } +} diff --git a/packages/shell/src/api/event.ts b/packages/shell/src/api/event.ts new file mode 100644 index 0000000000..f2adca98c1 --- /dev/null +++ b/packages/shell/src/api/event.ts @@ -0,0 +1,88 @@ +import { IEditor, IEventBus } from '@alilc/lowcode-editor-core'; +import { getLogger, isPluginEventName } from '@alilc/lowcode-utils'; +import { IPublicApiEvent, IPublicTypeDisposable } from '@alilc/lowcode-types'; + +const logger = getLogger({ level: 'warn', bizName: 'shell-event' }); + +type EventOptions = { + prefix: string; +}; + +const eventBusSymbol = Symbol('eventBus'); + +export class Event implements IPublicApiEvent { + private readonly [eventBusSymbol]: IEventBus; + private readonly options: EventOptions; + + constructor(eventBus: IEventBus, options: EventOptions, public workspaceMode = false) { + this[eventBusSymbol] = eventBus; + this.options = options; + if (!this.options.prefix) { + logger.warn('prefix is required while initializing Event'); + } + } + + /** + * 监听事件 + * @param event 事件名称 + * @param listener 事件回调 + */ + on(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable { + if (isPluginEventName(event)) { + return this[eventBusSymbol].on(event, listener); + } else { + logger.warn(`fail to monitor on event ${event}, event should have a prefix like 'somePrefix:eventName'`); + return () => {}; + } + } + + /** + * 监听事件,会在其他回调函数之前执行 + * @param event 事件名称 + * @param listener 事件回调 + */ + prependListener(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable { + if (isPluginEventName(event)) { + return this[eventBusSymbol].prependListener(event, listener); + } else { + logger.warn(`fail to prependListener event ${event}, event should have a prefix like 'somePrefix:eventName'`); + return () => {}; + } + } + + /** + * 取消监听事件 + * @param event 事件名称 + * @param listener 事件回调 + */ + off(event: string, listener: (...args: any[]) => void) { + this[eventBusSymbol].off(event, listener); + } + + /** + * 触发事件 + * @param event 事件名称 + * @param args 事件参数 + * @returns + */ + emit(event: string, ...args: any[]) { + if (!this.options.prefix) { + logger.warn('Event#emit has been forbidden while prefix is not specified'); + return; + } + this[eventBusSymbol].emit(`${this.options.prefix}:${event}`, ...args); + } + + /** + * DO NOT USE if u fully understand what this method does. + * @param event + * @param args + */ + __internalEmit__(event: string, ...args: unknown[]) { + this[eventBusSymbol].emit(event, ...args); + } +} + +export function getEvent(editor: IEditor, options: any = { prefix: 'common' }) { + return new Event(editor.eventBus, options); +} diff --git a/packages/shell/src/api/hotkey.ts b/packages/shell/src/api/hotkey.ts new file mode 100644 index 0000000000..4e65844ceb --- /dev/null +++ b/packages/shell/src/api/hotkey.ts @@ -0,0 +1,53 @@ +import { globalContext, Hotkey as InnerHotkey } from '@alilc/lowcode-editor-core'; +import { hotkeySymbol } from '../symbols'; +import { IPublicTypeDisposable, IPublicTypeHotkeyCallback, IPublicTypeHotkeyCallbacks, IPublicApiHotkey } from '@alilc/lowcode-types'; + +const innerHotkeySymbol = Symbol('innerHotkey'); + +export class Hotkey implements IPublicApiHotkey { + private readonly [innerHotkeySymbol]: InnerHotkey; + get [hotkeySymbol](): InnerHotkey { + if (this.workspaceMode) { + return this[innerHotkeySymbol]; + } + const workspace = globalContext.get('workspace'); + if (workspace.isActive) { + return workspace.window.innerHotkey; + } + + return this[innerHotkeySymbol]; + } + + constructor(hotkey: InnerHotkey, readonly workspaceMode: boolean = false) { + this[innerHotkeySymbol] = hotkey; + } + + get callbacks(): IPublicTypeHotkeyCallbacks { + return this[hotkeySymbol].callBacks; + } + + /** + * @deprecated + */ + get callBacks() { + return this.callbacks; + } + + /** + * 绑定快捷键 + * @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等 + * @param callback 回调函数 + * @param action + * @returns + */ + bind( + combos: string[] | string, + callback: IPublicTypeHotkeyCallback, + action?: string, + ): IPublicTypeDisposable { + this[hotkeySymbol].bind(combos, callback, action); + return () => { + this[hotkeySymbol].unbind(combos, callback, action); + }; + } +} \ No newline at end of file diff --git a/packages/shell/src/api/index.ts b/packages/shell/src/api/index.ts new file mode 100644 index 0000000000..79340f6777 --- /dev/null +++ b/packages/shell/src/api/index.ts @@ -0,0 +1,15 @@ +export * from './common'; +export * from './event'; +export * from './hotkey'; +export * from './logger'; +export * from './material'; +export * from './plugins'; +export * from './project'; +export * from './setters'; +export * from './simulator-host'; +export * from './skeleton'; +export * from './canvas'; +export * from './workspace'; +export * from './config'; +export * from './commonUI'; +export * from './command'; \ No newline at end of file diff --git a/packages/shell/src/api/logger.ts b/packages/shell/src/api/logger.ts new file mode 100644 index 0000000000..54fee7a660 --- /dev/null +++ b/packages/shell/src/api/logger.ts @@ -0,0 +1,48 @@ + +import { getLogger } from '@alilc/lowcode-utils'; +import { IPublicApiLogger, ILoggerOptions } from '@alilc/lowcode-types'; + +const innerLoggerSymbol = Symbol('logger'); + +export class Logger implements IPublicApiLogger { + private readonly [innerLoggerSymbol]: any; + + constructor(options: ILoggerOptions) { + this[innerLoggerSymbol] = getLogger(options as any); + } + + /** + * debug info + */ + debug(...args: any | any[]): void { + this[innerLoggerSymbol].debug(...args); + } + + /** + * normal info output + */ + info(...args: any | any[]): void { + this[innerLoggerSymbol].info(...args); + } + + /** + * warning info output + */ + warn(...args: any | any[]): void { + this[innerLoggerSymbol].warn(...args); + } + + /** + * error info output + */ + error(...args: any | any[]): void { + this[innerLoggerSymbol].error(...args); + } + + /** + * normal log output + */ + log(...args: any | any[]): void { + this[innerLoggerSymbol].log(...args); + } +} \ No newline at end of file diff --git a/packages/shell/src/api/material.ts b/packages/shell/src/api/material.ts new file mode 100644 index 0000000000..284b88fbbf --- /dev/null +++ b/packages/shell/src/api/material.ts @@ -0,0 +1,213 @@ +import { globalContext } from '@alilc/lowcode-editor-core'; +import { + IDesigner, + isComponentMeta, +} from '@alilc/lowcode-designer'; +import { IPublicTypeAssetsJson, getLogger } from '@alilc/lowcode-utils'; +import { + IPublicTypeComponentAction, + IPublicTypeComponentMetadata, + IPublicApiMaterial, + IPublicTypeMetadataTransducer, + IPublicModelComponentMeta, + IPublicTypeNpmInfo, + IPublicModelEditor, + IPublicTypeDisposable, + IPublicTypeContextMenuAction, + IPublicTypeContextMenuItem, +} from '@alilc/lowcode-types'; +import { Workspace as InnerWorkspace } from '@alilc/lowcode-workspace'; +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; + + get [editorSymbol](): IPublicModelEditor { + if (this.workspaceMode) { + return this[innerEditorSymbol]; + } + const workspace: InnerWorkspace = globalContext.get('workspace'); + if (workspace.isActive) { + if (!workspace.window.editor) { + logger.error('Material api 调用时机出现问题,请检查'); + return this[innerEditorSymbol]; + } + return workspace.window.editor; + } + + return this[innerEditorSymbol]; + } + + get [designerSymbol](): IDesigner { + return this[editorSymbol].get('designer')!; + } + + constructor(editor: IPublicModelEditor, readonly workspaceMode: boolean = false) { + this[innerEditorSymbol] = editor; + } + + /** + * 获取组件 map 结构 + */ + get componentsMap(): { [key: string]: IPublicTypeNpmInfo | ComponentType<any> | object } { + return this[designerSymbol].componentsMap; + } + + /** + * 设置「资产包」结构 + * @param assets + * @returns + */ + async setAssets(assets: IPublicTypeAssetsJson) { + return await this[editorSymbol].setAssets(assets); + } + + /** + * 获取「资产包」结构 + * @returns + */ + getAssets(): IPublicTypeAssetsJson | undefined { + return this[editorSymbol].get('assets'); + } + + /** + * 加载增量的「资产包」结构,该增量包会与原有的合并 + * @param incrementalAssets + * @returns + */ + loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson) { + return this[designerSymbol].loadIncrementalAssets(incrementalAssets); + } + + /** + * 注册物料元数据管道函数 + * @param transducer + * @param level + * @param id + */ + registerMetadataTransducer = ( + transducer: IPublicTypeMetadataTransducer, + level?: number, + id?: string | undefined, + ) => { + this[designerSymbol].componentActions.registerMetadataTransducer(transducer, level, id); + }; + + /** + * 获取所有物料元数据管道函数 + * @returns + */ + getRegisteredMetadataTransducers() { + return this[designerSymbol].componentActions.getRegisteredMetadataTransducers(); + } + + /** + * 获取指定名称的物料元数据 + * @param componentName + * @returns + */ + getComponentMeta(componentName: string): IPublicModelComponentMeta | null { + const innerMeta = this[designerSymbol].getComponentMeta(componentName); + return ShellComponentMeta.create(innerMeta); + } + + /** + * create an instance of ComponentMeta by given metadata + * @param metadata + * @returns + */ + createComponentMeta(metadata: IPublicTypeComponentMetadata) { + return ShellComponentMeta.create(this[designerSymbol].createComponentMeta(metadata)); + } + + /** + * test if the given object is a ComponentMeta instance or not + * @param obj + * @returns + */ + isComponentMeta(obj: any) { + return isComponentMeta(obj); + } + + /** + * 获取所有已注册的物料元数据 + * @returns + */ + getComponentMetasMap(): Map<string, IPublicModelComponentMeta> { + const map = new Map<string, IPublicModelComponentMeta>(); + const originalMap = this[designerSymbol].getComponentMetasMap(); + for (let componentName of originalMap.keys()) { + map.set(componentName, this.getComponentMeta(componentName)!); + } + return map; + } + + /** + * 在设计器辅助层增加一个扩展 action + * @param action + */ + addBuiltinComponentAction = (action: IPublicTypeComponentAction) => { + this[designerSymbol].componentActions.addBuiltinComponentAction(action); + }; + + /** + * 刷新 componentMetasMap,可触发模拟器里的 components 重新构建 + */ + refreshComponentMetasMap = () => { + this[designerSymbol].refreshComponentMetasMap(); + }; + + /** + * 移除设计器辅助层的指定 action + * @param name + */ + removeBuiltinComponentAction(name: string) { + this[designerSymbol].componentActions.removeBuiltinComponentAction(name); + } + + /** + * 修改已有的设计器辅助层的指定 action + * @param actionName + * @param handle + */ + modifyBuiltinComponentAction( + actionName: string, + handle: (action: IPublicTypeComponentAction) => void, + ) { + this[designerSymbol].componentActions.modifyBuiltinComponentAction(actionName, handle); + } + + /** + * 监听 assets 变化的事件 + * @param fn + */ + onChangeAssets(fn: () => void): IPublicTypeDisposable { + const dispose = [ + // 设置 assets,经过 setAssets 赋值 + this[editorSymbol].onChange('assets', fn), + // 增量设置 assets,经过 loadIncrementalAssets 赋值 + this[editorSymbol].eventBus.on('designer.incrementalAssetsReady', fn), + ]; + + return () => { + dispose.forEach(d => d && d()); + }; + } + + addContextMenuOption(option: IPublicTypeContextMenuAction) { + this[designerSymbol].contextMenuActions.addMenuAction(option); + } + + removeContextMenuOption(name: string) { + this[designerSymbol].contextMenuActions.removeMenuAction(name); + } + + adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]) { + this[designerSymbol].contextMenuActions.adjustMenuLayout(fn); + } +} diff --git a/packages/shell/src/api/plugins.ts b/packages/shell/src/api/plugins.ts new file mode 100644 index 0000000000..b6f5e63717 --- /dev/null +++ b/packages/shell/src/api/plugins.ts @@ -0,0 +1,88 @@ +import { + ILowCodePluginManager, +} from '@alilc/lowcode-designer'; +import { globalContext } from '@alilc/lowcode-editor-core'; +import { + IPublicApiPlugins, + IPublicModelPluginInstance, + IPublicTypePlugin, + IPublicTypePluginRegisterOptions, + IPublicTypePreferenceValueType, +} from '@alilc/lowcode-types'; +import { PluginInstance as ShellPluginInstance } from '../model'; +import { pluginsSymbol } from '../symbols'; + +const innerPluginsSymbol = Symbol('plugin'); +export class Plugins implements IPublicApiPlugins { + private readonly [innerPluginsSymbol]: ILowCodePluginManager; + get [pluginsSymbol](): ILowCodePluginManager { + if (this.workspaceMode) { + return this[innerPluginsSymbol]; + } + const workspace = globalContext.get('workspace'); + if (workspace.isActive) { + return workspace.window.innerPlugins; + } + + return this[innerPluginsSymbol]; + } + + constructor(plugins: ILowCodePluginManager, public workspaceMode: boolean = false) { + this[innerPluginsSymbol] = plugins; + } + + async register( + pluginModel: IPublicTypePlugin, + options?: any, + registerOptions?: IPublicTypePluginRegisterOptions, + ): Promise<void> { + await this[pluginsSymbol].register(pluginModel, options, registerOptions); + } + + async init(registerOptions: any) { + await this[pluginsSymbol].init(registerOptions); + } + + getPluginPreference( + pluginName: string, + ): Record<string, IPublicTypePreferenceValueType> | null | undefined { + return this[pluginsSymbol].getPluginPreference(pluginName); + } + + get(pluginName: string): IPublicModelPluginInstance | null { + const instance = this[pluginsSymbol].get(pluginName); + if (instance) { + return new ShellPluginInstance(instance); + } + + return null; + } + + getAll() { + return this[pluginsSymbol].getAll()?.map((d) => new ShellPluginInstance(d)); + } + + has(pluginName: string) { + return this[pluginsSymbol].has(pluginName); + } + + async delete(pluginName: string) { + return await this[pluginsSymbol].delete(pluginName); + } + + toProxy() { + return new Proxy(this, { + get(target, prop, receiver) { + const _target = target[pluginsSymbol]; + if (_target.pluginsMap.has(prop as string)) { + // 禁用态的插件,直接返回 undefined + if (_target.pluginsMap.get(prop as string)!.disabled) { + return undefined; + } + return _target.pluginsMap.get(prop as string)?.toProxy(); + } + return Reflect.get(target, prop, receiver); + }, + }); + } +} diff --git a/packages/shell/src/api/project.ts b/packages/shell/src/api/project.ts new file mode 100644 index 0000000000..f005d0af0c --- /dev/null +++ b/packages/shell/src/api/project.ts @@ -0,0 +1,246 @@ +import { + BuiltinSimulatorHost, + IProject as InnerProject, +} from '@alilc/lowcode-designer'; +import { globalContext } from '@alilc/lowcode-editor-core'; +import { + IPublicTypeRootSchema, + IPublicTypeProjectSchema, + IPublicModelEditor, + IPublicApiProject, + IPublicApiSimulatorHost, + IPublicModelDocumentModel, + IPublicTypePropsTransducer, + IPublicEnumTransformStage, + IPublicTypeDisposable, + IPublicTypeAppConfig, +} from '@alilc/lowcode-types'; +import { DocumentModel as ShellDocumentModel } from '../model'; +import { SimulatorHost } from './simulator-host'; +import { editorSymbol, projectSymbol, simulatorHostSymbol, documentSymbol } from '../symbols'; +import { getLogger } from '@alilc/lowcode-utils'; + +const logger = getLogger({ level: 'warn', bizName: 'shell-project' }); + +const innerProjectSymbol = Symbol('innerProject'); +export class Project implements IPublicApiProject { + private readonly [innerProjectSymbol]: InnerProject; + private [simulatorHostSymbol]: BuiltinSimulatorHost; + get [projectSymbol](): InnerProject { + if (this.workspaceMode) { + return this[innerProjectSymbol]; + } + const workspace = globalContext.get('workspace'); + if (workspace.isActive) { + if (!workspace.window?.innerProject) { + logger.error('project api 调用时机出现问题,请检查'); + return this[innerProjectSymbol]; + } + return workspace.window.innerProject; + } + + return this[innerProjectSymbol]; + } + + get [editorSymbol](): IPublicModelEditor { + return this[projectSymbol]?.designer.editor; + } + + constructor(project: InnerProject, public workspaceMode: boolean = false) { + this[innerProjectSymbol] = project; + } + + static create(project: InnerProject, workspaceMode: boolean = false) { + return new Project(project, workspaceMode); + } + + /** + * 获取当前的 document + * @returns + */ + get currentDocument(): IPublicModelDocumentModel | null { + return this.getCurrentDocument(); + } + + /** + * 获取当前 project 下所有 documents + * @returns + */ + get documents(): IPublicModelDocumentModel[] { + return this[projectSymbol].documents.map((doc) => ShellDocumentModel.create(doc)!); + } + + /** + * 获取模拟器的 host + */ + get simulatorHost(): IPublicApiSimulatorHost | null { + return SimulatorHost.create(this[projectSymbol].simulator as any || this[simulatorHostSymbol]); + } + + /** + * @deprecated use .simulatorHost instead. + */ + get simulator() { + return this.simulatorHost; + } + + /** + * 打开一个 document + * @param doc + * @returns + */ + openDocument(doc?: string | IPublicTypeRootSchema | undefined) { + const documentModel = this[projectSymbol].open(doc); + if (!documentModel) { + return null; + } + return ShellDocumentModel.create(documentModel); + } + + /** + * 创建一个 document + * @param data + * @returns + */ + createDocument(data?: IPublicTypeRootSchema): IPublicModelDocumentModel | null { + const doc = this[projectSymbol].createDocument(data); + return ShellDocumentModel.create(doc); + } + + /** + * 删除一个 document + * @param doc + */ + removeDocument(doc: IPublicModelDocumentModel) { + this[projectSymbol].removeDocument((doc as any)[documentSymbol]); + } + + /** + * 根据 fileName 获取 document + * @param fileName + * @returns + */ + getDocumentByFileName(fileName: string): IPublicModelDocumentModel | null { + const innerDocumentModel = this[projectSymbol].getDocumentByFileName(fileName); + return ShellDocumentModel.create(innerDocumentModel); + } + + /** + * 根据 id 获取 document + * @param id + * @returns + */ + getDocumentById(id: string): IPublicModelDocumentModel | null { + return ShellDocumentModel.create(this[projectSymbol].getDocument(id)); + } + + /** + * 导出 project + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render) { + return this[projectSymbol].getSchema(stage); + } + + /** + * 导入 project + * @param schema 待导入的 project 数据 + */ + importSchema(schema?: IPublicTypeProjectSchema): void { + this[projectSymbol].load(schema, true); + } + + /** + * 获取当前的 document + * @returns + */ + getCurrentDocument(): IPublicModelDocumentModel | null { + return ShellDocumentModel.create(this[projectSymbol].currentDocument); + } + + /** + * 增加一个属性的管道处理函数 + * @param transducer + * @param stage + */ + addPropsTransducer( + transducer: IPublicTypePropsTransducer, + stage: IPublicEnumTransformStage, + ): void { + this[projectSymbol].designer.addPropsReducer(transducer, stage); + } + + /** + * 绑定删除文档事件 + * @param fn + * @returns + */ + onRemoveDocument(fn: (data: { id: string}) => void): IPublicTypeDisposable { + return this[editorSymbol].eventBus.on( + 'designer.document.remove', + (data: { id: string }) => fn(data), + ); + } + + /** + * 当前 project 内的 document 变更事件 + */ + onChangeDocument(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable { + const offFn = this[projectSymbol].onCurrentDocumentChange((originalDoc) => { + fn(ShellDocumentModel.create(originalDoc)!); + }); + if (this[projectSymbol].currentDocument) { + fn(ShellDocumentModel.create(this[projectSymbol].currentDocument)!); + } + return offFn; + } + + /** + * 当前 project 的模拟器 ready 事件 + */ + onSimulatorHostReady(fn: (host: IPublicApiSimulatorHost) => void): IPublicTypeDisposable { + const offFn = this[projectSymbol].onSimulatorReady((simulator: BuiltinSimulatorHost) => { + fn(SimulatorHost.create(simulator)!); + }); + return offFn; + } + + /** + * 当前 project 的渲染器 ready 事件 + */ + onSimulatorRendererReady(fn: () => void): IPublicTypeDisposable { + const offFn = this[projectSymbol].onRendererReady(() => { + fn(); + }); + return offFn; + } + + /** + * 设置多语言语料 + * 数据格式参考 https://github.com/alibaba/lowcode-engine/blob/main/specs/lowcode-spec.md#2434%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E7%B1%BB%E5%9E%8Baa + * @param value object + * @returns + */ + setI18n(value: object): void { + this[projectSymbol].set('i18n', value); + } + + /** + * 设置项目配置 + * @param value object + * @returns + */ + setConfig<T extends keyof IPublicTypeAppConfig>(key: T, value: IPublicTypeAppConfig[T]): void; + setConfig(value: IPublicTypeAppConfig): void; + setConfig(...params: any[]): void { + if (params.length === 2) { + const oldConfig = this[projectSymbol].get('config'); + this[projectSymbol].set('config', { + ...oldConfig, + [params[0]]: params[1], + }); + } else { + this[projectSymbol].set('config', params[0]); + } + } +} diff --git a/packages/shell/src/api/setters.ts b/packages/shell/src/api/setters.ts new file mode 100644 index 0000000000..b7f2d40ecf --- /dev/null +++ b/packages/shell/src/api/setters.ts @@ -0,0 +1,75 @@ +import { IPublicTypeCustomView, IPublicApiSetters, IPublicTypeRegisteredSetter } from '@alilc/lowcode-types'; +import { ISetters, globalContext, untracked } from '@alilc/lowcode-editor-core'; +import { ReactNode } from 'react'; +import { getLogger } from '@alilc/lowcode-utils'; + +const innerSettersSymbol = Symbol('setters'); +const settersSymbol = Symbol('setters'); + +const logger = getLogger({ level: 'warn', bizName: 'shell-setters' }); + +export class Setters implements IPublicApiSetters { + readonly [innerSettersSymbol]: ISetters; + + get [settersSymbol](): ISetters { + if (this.workspaceMode) { + return this[innerSettersSymbol]; + } + + const workspace = globalContext.get('workspace'); + if (workspace.isActive) { + return untracked(() => { + if (!workspace.window?.innerSetters) { + logger.error('setter api 调用时机出现问题,请检查'); + return this[innerSettersSymbol]; + } + return workspace.window.innerSetters; + }); + } + + return this[innerSettersSymbol]; + } + + constructor(innerSetters: ISetters, readonly workspaceMode = false) { + this[innerSettersSymbol] = innerSetters; + } + + /** + * 获取指定 setter + * @param type + * @returns + */ + getSetter = (type: string) => { + return this[settersSymbol].getSetter(type); + }; + + /** + * 获取已注册的所有 settersMap + * @returns + */ + getSettersMap = (): Map<string, IPublicTypeRegisteredSetter & { + type: string; + }> => { + return this[settersSymbol].getSettersMap(); + }; + + /** + * 注册一个 setter + * @param typeOrMaps + * @param setter + * @returns + */ + registerSetter = ( + typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, + setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter | undefined, + ) => { + return this[settersSymbol].registerSetter(typeOrMaps, setter); + }; + + /** + * @deprecated + */ + createSetterContent = (setter: any, props: Record<string, any>): ReactNode => { + return this[settersSymbol].createSetterContent(setter, props); + }; +} diff --git a/packages/shell/src/api/simulator-host.ts b/packages/shell/src/api/simulator-host.ts new file mode 100644 index 0000000000..663ba0c668 --- /dev/null +++ b/packages/shell/src/api/simulator-host.ts @@ -0,0 +1,74 @@ +import { + BuiltinSimulatorHost, +} from '@alilc/lowcode-designer'; +import { simulatorHostSymbol, nodeSymbol } from '../symbols'; +import { IPublicApiSimulatorHost, IPublicModelNode, IPublicModelSimulatorRender } from '@alilc/lowcode-types'; +import { SimulatorRender } from '../model/simulator-render'; + +export class SimulatorHost implements IPublicApiSimulatorHost { + private readonly [simulatorHostSymbol]: BuiltinSimulatorHost; + + constructor(simulator: BuiltinSimulatorHost) { + this[simulatorHostSymbol] = simulator; + } + + static create(host: BuiltinSimulatorHost): IPublicApiSimulatorHost | null { + if (!host) return null; + return new SimulatorHost(host); + } + + /** + * 获取 contentWindow + */ + get contentWindow(): Window | undefined { + return this[simulatorHostSymbol].contentWindow; + } + + /** + * 获取 contentDocument + */ + get contentDocument(): Document | undefined { + return this[simulatorHostSymbol].contentDocument; + } + + get renderer(): IPublicModelSimulatorRender | undefined { + if (this[simulatorHostSymbol].renderer) { + return SimulatorRender.create(this[simulatorHostSymbol].renderer); + } + + return undefined; + } + + /** + * 设置 host 配置值 + * @param key + * @param value + */ + set(key: string, value: any): void { + this[simulatorHostSymbol].set(key, value); + } + + /** + * 获取 host 配置值 + * @param key + * @returns + */ + get(key: string): any { + return this[simulatorHostSymbol].get(key); + } + + /** + * scroll to specific node + * @param node + */ + scrollToNode(node: IPublicModelNode): void { + this[simulatorHostSymbol].scrollToNode((node as any)[nodeSymbol]); + } + + /** + * 触发组件构建,并刷新渲染画布 + */ + rerender(): void { + this[simulatorHostSymbol].rerender(); + } +} diff --git a/packages/shell/src/api/skeleton.ts b/packages/shell/src/api/skeleton.ts new file mode 100644 index 0000000000..c61edf95d0 --- /dev/null +++ b/packages/shell/src/api/skeleton.ts @@ -0,0 +1,257 @@ +import { globalContext } from '@alilc/lowcode-editor-core'; +import { + ISkeleton, + SkeletonEvents, +} from '@alilc/lowcode-editor-skeleton'; +import { skeletonSymbol } from '../symbols'; +import { IPublicApiSkeleton, IPublicModelSkeletonItem, IPublicTypeConfigTransducer, IPublicTypeDisposable, IPublicTypeSkeletonConfig, IPublicTypeWidgetConfigArea } from '@alilc/lowcode-types'; +import { getLogger } from '@alilc/lowcode-utils'; +import { SkeletonItem } from '../model/skeleton-item'; + +const innerSkeletonSymbol = Symbol('skeleton'); + +const logger = getLogger({ level: 'warn', bizName: 'shell-skeleton' }); + +export class Skeleton implements IPublicApiSkeleton { + private readonly [innerSkeletonSymbol]: ISkeleton; + private readonly pluginName: string; + + get [skeletonSymbol](): ISkeleton { + if (this.workspaceMode) { + return this[innerSkeletonSymbol]; + } + const workspace = globalContext.get('workspace'); + if (workspace.isActive) { + if (!workspace.window?.innerSkeleton) { + logger.error('skeleton api 调用时机出现问题,请检查'); + return this[innerSkeletonSymbol]; + } + return workspace.window.innerSkeleton; + } + + return this[innerSkeletonSymbol]; + } + + constructor( + skeleton: ISkeleton, + pluginName: string, + readonly workspaceMode: boolean = false, + ) { + this[innerSkeletonSymbol] = skeleton; + this.pluginName = pluginName; + } + + /** + * 增加一个面板实例 + * @param config + * @param extraConfig + * @returns + */ + add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IPublicModelSkeletonItem | undefined { + const configWithName = { + ...config, + pluginName: this.pluginName, + }; + const item = this[skeletonSymbol].add(configWithName, extraConfig); + if (item) { + return new SkeletonItem(item); + } + } + + /** + * 移除一个面板实例 + * @param config + * @returns + */ + remove(config: IPublicTypeSkeletonConfig): number | undefined { + const { area, name } = config; + const skeleton = this[skeletonSymbol]; + if (!normalizeArea(area)) { + return; + } + skeleton[normalizeArea(area)].container?.remove(name); + } + + getAreaItems(areaName: IPublicTypeWidgetConfigArea): IPublicModelSkeletonItem[] { + return this[skeletonSymbol][normalizeArea(areaName)].container.items?.map(d => new SkeletonItem(d)); + } + + getPanel(name: string) { + const item = this[skeletonSymbol].getPanel(name); + if (!item) { + return; + } + + return new SkeletonItem(item); + } + + /** + * 显示面板 + * @param name + */ + showPanel(name: string) { + this[skeletonSymbol].getPanel(name)?.show(); + } + + /** + * 隐藏面板 + * @param name + */ + hidePanel(name: string) { + this[skeletonSymbol].getPanel(name)?.hide(); + } + + /** + * 显示 widget + * @param name + */ + showWidget(name: string) { + this[skeletonSymbol].getWidget(name)?.show(); + } + + /** + * enable widget + * @param name + */ + enableWidget(name: string) { + this[skeletonSymbol].getWidget(name)?.enable?.(); + } + + /** + * 隐藏 widget + * @param name + */ + hideWidget(name: string) { + this[skeletonSymbol].getWidget(name)?.hide(); + } + + /** + * disable widget,不可点击 + * @param name + */ + disableWidget(name: string) { + this[skeletonSymbol].getWidget(name)?.disable?.(); + } + + /** + * show area + * @param areaName name of area + */ + showArea(areaName: string) { + (this[skeletonSymbol] as any)[areaName]?.show(); + } + + /** + * hide area + * @param areaName name of area + */ + hideArea(areaName: string) { + (this[skeletonSymbol] as any)[areaName]?.hide(); + } + + /** + * 监听 panel 显示事件 + * @param listener + * @returns + */ + onShowPanel(listener: (paneName: string, panel: IPublicModelSkeletonItem) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.PANEL_SHOW, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.PANEL_SHOW, listener); + } + + onDisableWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.WIDGET_DISABLE, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.WIDGET_DISABLE, listener); + } + + onEnableWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.WIDGET_ENABLE, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.WIDGET_ENABLE, listener); + } + + /** + * 监听 panel 隐藏事件 + * @param listener + * @returns + */ + onHidePanel(listener: (...args: any[]) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.PANEL_HIDE, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.PANEL_HIDE, listener); + } + + /** + * 监听 widget 显示事件 + * @param listener + * @returns + */ + onShowWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.WIDGET_SHOW, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.WIDGET_SHOW, listener); + } + + /** + * 监听 widget 隐藏事件 + * @param listener + * @returns + */ + onHideWidget(listener: (...args: any[]) => void): IPublicTypeDisposable { + const { editor } = this[skeletonSymbol]; + editor.eventBus.on(SkeletonEvents.WIDGET_HIDE, (name: any, panel: any) => { + listener(name, new SkeletonItem(panel)); + }); + return () => editor.eventBus.off(SkeletonEvents.WIDGET_HIDE, listener); + } + + registerConfigTransducer(fn: IPublicTypeConfigTransducer, level: number, id?: string) { + this[skeletonSymbol].registerConfigTransducer(fn, level, id); + } +} + +function normalizeArea(area: IPublicTypeWidgetConfigArea | undefined): 'leftArea' | 'rightArea' | 'topArea' | 'toolbar' | 'mainArea' | 'bottomArea' | 'leftFixedArea' | 'leftFloatArea' | 'stages' | 'subTopArea' { + switch (area) { + case 'leftArea': + case 'left': + return 'leftArea'; + case 'rightArea': + case 'right': + return 'rightArea'; + case 'topArea': + case 'top': + return 'topArea'; + case 'toolbar': + return 'toolbar'; + case 'mainArea': + case 'main': + case 'center': + case 'centerArea': + return 'mainArea'; + case 'bottomArea': + case 'bottom': + return 'bottomArea'; + case 'leftFixedArea': + return 'leftFixedArea'; + case 'leftFloatArea': + return 'leftFloatArea'; + case 'stages': + return 'stages'; + case 'subTopArea': + return 'subTopArea'; + default: + throw new Error(`${area} not supported`); + } +} diff --git a/packages/shell/src/api/workspace.ts b/packages/shell/src/api/workspace.ts new file mode 100644 index 0000000000..f5bc79009f --- /dev/null +++ b/packages/shell/src/api/workspace.ts @@ -0,0 +1,115 @@ +import { IPublicApiWorkspace, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; +import { IWorkspace } from '@alilc/lowcode-workspace'; +import { resourceSymbol, workspaceSymbol } from '../symbols'; +import { Resource as ShellResource, Window as ShellWindow } from '../model'; +import { Plugins } from './plugins'; +import { Skeleton } from './skeleton'; + +export class Workspace implements IPublicApiWorkspace { + readonly [workspaceSymbol]: IWorkspace; + + constructor(innerWorkspace: IWorkspace) { + this[workspaceSymbol] = innerWorkspace; + } + + get resourceList() { + return this[workspaceSymbol].getResourceList().map((d) => new ShellResource(d)); + } + + setResourceList(resourceList: IPublicResourceList) { + this[workspaceSymbol].setResourceList(resourceList); + } + + onResourceListChange(fn: (resourceList: IPublicResourceList) => void): IPublicTypeDisposable { + return this[workspaceSymbol].onResourceListChange(fn); + } + + get isActive() { + return this[workspaceSymbol].isActive; + } + + get window() { + if (!this[workspaceSymbol].window) { + return null; + } + return new ShellWindow(this[workspaceSymbol].window); + } + + get resourceTypeList() { + return Array.from(this[workspaceSymbol].resourceTypeMap.values()).map((d) => { + const { name: resourceName, type: resourceType } = d; + const { + description, + editorViews, + } = d.resourceTypeModel({} as any, {}); + + return { + resourceName, + resourceType, + description, + editorViews: editorViews.map(d => ( + { + viewName: d.viewName, + viewType: d.viewType || 'editor', + } + )), + }; + }); + } + + onWindowRendererReady(fn: () => void): IPublicTypeDisposable { + return this[workspaceSymbol].onWindowRendererReady(fn); + } + + registerResourceType(resourceTypeModel: IPublicTypeResourceType): void { + this[workspaceSymbol].registerResourceType(resourceTypeModel); + } + + async openEditorWindow(): Promise<void> { + if (typeof arguments[0] === 'string') { + await this[workspaceSymbol].openEditorWindow(arguments[0], arguments[1], arguments[2], arguments[3], arguments[4]); + } else { + await this[workspaceSymbol].openEditorWindowByResource(arguments[0]?.[resourceSymbol], arguments[1]); + } + } + + openEditorWindowById(id: string) { + this[workspaceSymbol].openEditorWindowById(id); + } + + removeEditorWindow() { + if (typeof arguments[0] === 'string') { + this[workspaceSymbol].removeEditorWindow(arguments[0], arguments[1]); + } else { + this[workspaceSymbol].removeEditorWindowByResource(arguments[0]?.[resourceSymbol]); + } + } + + removeEditorWindowById(id: string) { + this[workspaceSymbol].removeEditorWindowById(id); + } + + get plugins() { + return new Plugins(this[workspaceSymbol].plugins, true).toProxy(); + } + + get skeleton() { + return new Skeleton(this[workspaceSymbol].skeleton, 'workspace', true); + } + + get windows() { + return this[workspaceSymbol].windows.map((d) => new ShellWindow(d)); + } + + onChangeWindows(fn: () => void): IPublicTypeDisposable { + return this[workspaceSymbol].onChangeWindows(fn); + } + + onChangeActiveWindow(fn: () => void): IPublicTypeDisposable { + return this[workspaceSymbol].onChangeActiveWindow(fn); + } + + onChangeActiveEditorView(fn: () => void): IPublicTypeDisposable { + return this[workspaceSymbol].onChangeActiveEditorView(fn); + } +} diff --git a/packages/shell/src/canvas.ts b/packages/shell/src/canvas.ts deleted file mode 100644 index 268dbc4a8f..0000000000 --- a/packages/shell/src/canvas.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Designer } from '@alilc/lowcode-designer'; -import { designerSymbol } from './symbols'; -import DropLocation from './drop-location'; - -export default class Canvas { - private readonly [designerSymbol]: Designer; - - constructor(designer: Designer) { - this[designerSymbol] = designer; - } - - static create(designer: Designer) { - if (!designer) return null; - return new Canvas(designer); - } - - get dropLocation() { - return DropLocation.create(this[designerSymbol].dropLocation || null); - } -} \ No newline at end of file diff --git a/packages/shell/src/component-meta.ts b/packages/shell/src/component-meta.ts deleted file mode 100644 index e7752f6858..0000000000 --- a/packages/shell/src/component-meta.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { - ComponentMeta as InnerComponentMeta, - ParentalNode, -} from '@alilc/lowcode-designer'; -import Node from './node'; -import { NodeData, NodeSchema } from '@alilc/lowcode-types'; -import { componentMetaSymbol, nodeSymbol } from './symbols'; - -export default class ComponentMeta { - private readonly [componentMetaSymbol]: InnerComponentMeta; - - constructor(componentMeta: InnerComponentMeta) { - this[componentMetaSymbol] = componentMeta; - } - - static create(componentMeta: InnerComponentMeta | null) { - if (!componentMeta) return null; - return new ComponentMeta(componentMeta); - } - - /** - * 组件名 - */ - get componentName(): string { - return this[componentMetaSymbol].componentName; - } - - /** - * 是否是「容器型」组件 - */ - get isContainer(): boolean { - return this[componentMetaSymbol].isContainer; - } - - /** - * 是否是最小渲染单元。 - * 当组件需要重新渲染时: - * 若为最小渲染单元,则只渲染当前组件, - * 若不为最小渲染单元,则寻找到上层最近的最小渲染单元进行重新渲染,直至根节点。 - */ - get isMinimalRenderUnit(): boolean { - return this[componentMetaSymbol].isMinimalRenderUnit; - } - - /** - * 是否为「模态框」组件 - */ - get isModal(): boolean { - return this[componentMetaSymbol].isModal; - } - - /** - * 元数据配置 - */ - get configure() { - return this[componentMetaSymbol].configure; - } - - /** - * 标题 - */ - get title() { - return this[componentMetaSymbol].title; - } - - /** - * 图标 - */ - get icon() { - return this[componentMetaSymbol].icon; - } - - /** - * 组件 npm 信息 - */ - get npm() { - return this[componentMetaSymbol].npm; - } - - /** - * @deprecated - */ - get prototype() { - return this[componentMetaSymbol].prototype; - } - - get availableActions() { - return this[componentMetaSymbol].availableActions; - } - - /** - * 设置 npm 信息 - * @param npm - */ - setNpm(npm: any) { - this[componentMetaSymbol].setNpm(npm); - } - - /** - * 获取元数据 - * @returns - */ - getMetadata() { - return this[componentMetaSymbol].getMetadata(); - } - - /** - * check if the current node could be placed in parent node - * @param my - * @param parent - * @returns - */ - checkNestingUp(my: Node | NodeData, parent: ParentalNode<NodeSchema>) { - const curNode = my.isNode ? my[nodeSymbol] : my; - return this[componentMetaSymbol].checkNestingUp(curNode as any, parent); - } - - /** - * check if the target node(s) could be placed in current node - * @param my - * @param parent - * @returns - */ - checkNestingDown(my: Node | NodeData, target: NodeSchema | Node | NodeSchema[]) { - const curNode = my.isNode ? my[nodeSymbol] : my; - return this[componentMetaSymbol].checkNestingDown(curNode as any, target[nodeSymbol] || target); - } - - refreshMetadata() { - this[componentMetaSymbol].refreshMetadata(); - } -} diff --git a/packages/shell/src/components/context-menu.tsx b/packages/shell/src/components/context-menu.tsx new file mode 100644 index 0000000000..8c7ab446ba --- /dev/null +++ b/packages/shell/src/components/context-menu.tsx @@ -0,0 +1,72 @@ +import { createContextMenu, parseContextMenuAsReactNode, parseContextMenuProperties } from '@alilc/lowcode-utils'; +import { engineConfig } from '@alilc/lowcode-editor-core'; +import { IPublicModelPluginContext, IPublicTypeContextMenuAction } from '@alilc/lowcode-types'; +import React, { useCallback } from 'react'; + +export function ContextMenu({ children, menus, pluginContext }: { + menus: IPublicTypeContextMenuAction[]; + children: React.ReactElement[] | React.ReactElement; + pluginContext: IPublicModelPluginContext; +}): React.ReactElement<any, string | React.JSXElementConstructor<any>> { + const handleContextMenu = useCallback((event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + let destroyFn: Function | undefined; + const destroy = () => { + destroyFn?.(); + }; + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { + destroy, + pluginContext, + }), { pluginContext }); + + if (!children?.length) { + return; + } + + destroyFn = createContextMenu(children, { event }); + }, [menus]); + + if (!engineConfig.get('enableContextMenu')) { + return ( + <>{ children }</> + ); + } + + if (!menus) { + return ( + <>{ children }</> + ); + } + + // 克隆 children 并添加 onContextMenu 事件处理器 + const childrenWithContextMenu = React.Children.map(children, (child) => + React.cloneElement( + child, + { onContextMenu: handleContextMenu }, + )); + + return ( + <>{childrenWithContextMenu}</> + ); +} + +ContextMenu.create = (pluginContext: IPublicModelPluginContext, menus: IPublicTypeContextMenuAction[], event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const children: React.ReactNode[] = parseContextMenuAsReactNode(parseContextMenuProperties(menus, { + pluginContext, + }), { + pluginContext, + }); + + if (!children?.length) { + return; + } + + return createContextMenu(children, { + event, + }); +}; \ No newline at end of file diff --git a/packages/shell/src/detecting.ts b/packages/shell/src/detecting.ts deleted file mode 100644 index 998636542c..0000000000 --- a/packages/shell/src/detecting.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - Detecting as InnerDetecting, - DocumentModel as InnerDocumentModel, -} from '@alilc/lowcode-designer'; -import { documentSymbol, detectingSymbol } from './symbols'; - -export default class Detecting { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [detectingSymbol]: InnerDetecting; - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - this[detectingSymbol] = document.designer.detecting; - } - - /** - * hover 指定节点 - * @param id 节点 id - */ - capture(id: string) { - this[detectingSymbol].capture(this[documentSymbol].getNode(id)); - } - - /** - * hover 离开指定节点 - * @param id 节点 id - */ - release(id: string) { - this[detectingSymbol].release(this[documentSymbol].getNode(id)); - } - - /** - * 清空 hover 态 - */ - leave() { - this[detectingSymbol].leave(this[documentSymbol]); - } -} \ No newline at end of file diff --git a/packages/shell/src/document-model.ts b/packages/shell/src/document-model.ts deleted file mode 100644 index e4e79ddaeb..0000000000 --- a/packages/shell/src/document-model.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { Editor } from '@alilc/lowcode-editor-core'; -import { - DocumentModel as InnerDocumentModel, - Node as InnerNode, - ParentalNode, - IOnChangeOptions as InnerIOnChangeOptions, - PropChangeOptions as InnerPropChangeOptions, -} from '@alilc/lowcode-designer'; -import { - TransformStage, - RootSchema, - NodeSchema, - NodeData, - GlobalEvent, -} from '@alilc/lowcode-types'; -import Node from './node'; -import Selection from './selection'; -import Detecting from './detecting'; -import History from './history'; -import Project from './project'; -import Prop from './prop'; -import Canvas from './canvas'; -import ModalNodesManager from './modal-nodes-manager'; -import { documentSymbol, editorSymbol, nodeSymbol } from './symbols'; - -type IOnChangeOptions = { - type: string; - node: Node; -}; - -type PropChangeOptions = { - key?: string | number; - prop?: Prop; - node: Node; - newValue: any; - oldValue: any; -}; - -const Events = { - IMPORT_SCHEMA: 'shell.document.importSchema', -}; - -export default class DocumentModel { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [editorSymbol]: Editor; - private _focusNode: Node; - public selection: Selection; - public detecting: Detecting; - public history: History; - public canvas: Canvas; - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - this[editorSymbol] = document.designer.editor as Editor; - this.selection = new Selection(document); - this.detecting = new Detecting(document); - this.history = new History(document.getHistory()); - this.canvas = new Canvas(document.designer); - - this._focusNode = Node.create(this[documentSymbol].focusNode); - } - - static create(document: InnerDocumentModel | undefined | null) { - if (document == undefined) return null; - return new DocumentModel(document); - } - - /** - * id - */ - get id() { - return this[documentSymbol].id; - } - - set id(id) { - this[documentSymbol].id = id; - } - - /** - * 获取当前文档所属的 project - * @returns - */ - get project() { - return Project.create(this[documentSymbol].project); - } - - /** - * 获取文档的根节点 - * @returns - */ - get root(): Node | null { - return Node.create(this[documentSymbol].getRoot()); - } - - get focusNode(): Node { - return this._focusNode || this.root; - } - - set focusNode(node: Node) { - this._focusNode = node; - } - - /** - * 获取文档下所有节点 - * @returns - */ - get nodesMap() { - const map = new Map<string, Node>(); - for (let id of this[documentSymbol].nodesMap.keys()) { - map.set(id, this.getNodeById(id)!); - } - return map; - } - - /** - * 模态节点管理 - */ - get modalNodesManager() { - return ModalNodesManager.create(this[documentSymbol].modalNodesManager); - } - - // @TODO: 不能直接暴露 - get dropLocation() { - return this[documentSymbol].dropLocation; - } - - /** - * 根据 nodeId 返回 Node 实例 - * @param nodeId - * @returns - */ - getNodeById(nodeId: string) { - return Node.create(this[documentSymbol].getNode(nodeId)); - } - - /** - * 导入 schema - * @param schema - */ - importSchema(schema: RootSchema) { - this[documentSymbol].import(schema); - this[editorSymbol].emit(Events.IMPORT_SCHEMA, schema); - } - - /** - * 导出 schema - * @param stage - * @returns - */ - exportSchema(stage: TransformStage = TransformStage.Render) { - return this[documentSymbol].export(stage); - } - - /** - * 插入节点 - * @param parent - * @param thing - * @param at - * @param copy - * @returns - */ - insertNode( - parent: Node, - thing: Node, - at?: number | null | undefined, - copy?: boolean | undefined, - ) { - const node = this[documentSymbol].insertNode( - parent[nodeSymbol] ? parent[nodeSymbol] : parent, - thing?.[nodeSymbol] ? thing[nodeSymbol] : thing, - at, - copy, - ); - return Node.create(node); - } - - /** - * 创建一个节点 - * @param data - * @returns - */ - createNode(data: any) { - return Node.create(this[documentSymbol].createNode(data)); - } - - /** - * 移除指定节点/节点id - * @param idOrNode - */ - removeNode(idOrNode: string | Node) { - this[documentSymbol].removeNode(idOrNode as any); - } - - /** - * componentsMap of documentModel - * @param extraComps - * @returns - */ - getComponentsMap(extraComps?: string[]) { - return this[documentSymbol].getComponentsMap(extraComps); - } - - /** - * 当前 document 新增节点事件 - */ - onAddNode(fn: (node: Node) => void) { - return this[documentSymbol].onNodeCreate((node: InnerNode) => { - fn(Node.create(node)!); - }); - } - - /** - * 当前 document 新增节点事件,此时节点已经挂载到 document 上 - */ - onMountNode(fn: (payload: { node: Node }) => void) { - this[editorSymbol].on('node.add', fn as any); - return () => { - this[editorSymbol].off('node.add', fn as any); - }; - } - - /** - * 当前 document 删除节点事件 - */ - onRemoveNode(fn: (node: Node) => void) { - return this[documentSymbol].onNodeDestroy((node: InnerNode) => { - fn(Node.create(node)!); - }); - } - - /** - * 当前 document 的 hover 变更事件 - */ - onChangeDetecting(fn: (node: Node) => void) { - return this[documentSymbol].designer.detecting.onDetectingChange((node: InnerNode) => { - fn(Node.create(node)!); - }); - } - - /** - * 当前 document 的选中变更事件 - */ - onChangeSelection(fn: (ids: string[]) => void) { - return this[documentSymbol].selection.onSelectionChange((ids: string[]) => { - fn(ids); - }); - } - - /** - * 当前 document 的节点显隐状态变更事件 - * @param fn - */ - onChangeNodeVisible(fn: (node: Node, visible: boolean) => void) { - // TODO: history 变化时需要重新绑定 - this[documentSymbol].nodesMap.forEach((node) => { - node.onVisibleChange((flag: boolean) => { - fn(Node.create(node)!, flag); - }); - }); - } - - /** - * 当前 document 的节点 children 变更事件 - * @param fn - */ - onChangeNodeChildren(fn: (info?: IOnChangeOptions) => void) { - // TODO: history 变化时需要重新绑定 - this[documentSymbol].nodesMap.forEach((node) => { - node.onChildrenChange((info?: InnerIOnChangeOptions) => { - return info - ? fn({ - type: info.type, - node: Node.create(node)!, - }) - : fn(); - }); - }); - } - - /** - * 当前 document 节点属性修改事件 - * @param fn - */ - onChangeNodeProp(fn: (info: PropChangeOptions) => void) { - this[editorSymbol].on( - GlobalEvent.Node.Prop.InnerChange, - (info: GlobalEvent.Node.Prop.ChangeOptions) => { - fn({ - key: info.key, - oldValue: info.oldValue, - newValue: info.newValue, - prop: Prop.create(info.prop)!, - node: Node.create(info.node as any)!, - }); - }, - ); - } - - /** - * import schema event - * @param fn - */ - onImportSchema(fn: (schema: RootSchema) => void) { - this[editorSymbol].on(Events.IMPORT_SCHEMA, fn as any); - } -} diff --git a/packages/shell/src/drag-object.ts b/packages/shell/src/drag-object.ts deleted file mode 100644 index 1261b4d264..0000000000 --- a/packages/shell/src/drag-object.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DragObject as InnerDragObject, DragNodeDataObject } from '@alilc/lowcode-designer'; -import { dragObjectSymbol } from './symbols'; -import Node from './node'; - -export default class DragObject { - private readonly [dragObjectSymbol]: InnerDragObject; - - constructor(dragObject: InnerDragObject) { - this[dragObjectSymbol] = dragObject; - } - - static create(dragObject: InnerDragObject) { - if (!dragObject) return null; - return new DragObject(dragObject); - } - - get type() { - return this[dragObjectSymbol].type; - } - - get nodes() { - const { nodes } = this[dragObjectSymbol]; - if (!nodes) return null; - return nodes.map(Node.create); - } - - get data() { - return (this[dragObjectSymbol] as DragNodeDataObject).data; - } -} \ No newline at end of file diff --git a/packages/shell/src/dragon.ts b/packages/shell/src/dragon.ts deleted file mode 100644 index a1e4a4dab9..0000000000 --- a/packages/shell/src/dragon.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - Dragon as InnerDragon, - DragObject as InnerDragObject, - DragNodeDataObject, - LocateEvent as InnerLocateEvent, -} from '@alilc/lowcode-designer'; -import { dragonSymbol } from './symbols'; -import LocateEvent from './locate-event'; -import DragObject from './drag-object'; - -export default class Dragon { - private readonly [dragonSymbol]: InnerDragon; - - constructor(dragon: InnerDragon) { - this[dragonSymbol] = dragon; - } - - static create(dragon: InnerDragon | null) { - if (!dragon) return null; - return new Dragon(dragon); - } - - /** - * is dragging or not - */ - get dragging() { - return this[dragonSymbol].dragging; - } - - /** - * 绑定 dragstart 事件 - * @param func - * @returns - */ - onDragstart(func: (e: LocateEvent) => any) { - return this[dragonSymbol].onDragstart((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); - } - - /** - * 绑定 drag 事件 - * @param func - * @returns - */ - onDrag(func: (e: LocateEvent) => any) { - return this[dragonSymbol].onDrag((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); - } - - /** - * 绑定 dragend 事件 - * @param func - * @returns - */ - onDragend(func: (o: { dragObject: DragObject; copy?: boolean }) => any) { - return this[dragonSymbol].onDragend( - (o: { dragObject: InnerDragObject; copy?: boolean }) => func({ - dragObject: DragObject.create(o.dragObject)!, - copy: o.copy, - }), - ); - } - - /** - * 设置拖拽监听的区域 shell,以及自定义拖拽转换函数 boost - * @param shell 拖拽监听的区域 - * @param boost 拖拽转换函数 - */ - from(shell: Element, boost: (e: MouseEvent) => DragNodeDataObject | null) { - return this[dragonSymbol].from(shell, boost); - } -} diff --git a/packages/shell/src/drop-location.ts b/packages/shell/src/drop-location.ts deleted file mode 100644 index 14eff23edb..0000000000 --- a/packages/shell/src/drop-location.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - DropLocation as InnerDropLocation, -} from '@alilc/lowcode-designer'; -import { dropLocationSymbol } from './symbols'; -import Node from './node'; - -export default class DropLocation { - private readonly [dropLocationSymbol]: InnerDropLocation; - - constructor(dropLocation: InnerDropLocation) { - this[dropLocationSymbol] = dropLocation; - } - - static create(dropLocation: InnerDropLocation | null) { - if (!dropLocation) return null; - return new DropLocation(dropLocation); - } - - get target() { - return Node.create(this[dropLocationSymbol].target); - } -} diff --git a/packages/shell/src/event.ts b/packages/shell/src/event.ts deleted file mode 100644 index cb2242229f..0000000000 --- a/packages/shell/src/event.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { Editor as InnerEditor, globalContext } from '@alilc/lowcode-editor-core'; -import { getLogger } from '@alilc/lowcode-utils'; -import { editorSymbol } from './symbols'; - -const logger = getLogger({ level: 'warn', bizName: 'shell:event' }); - -type EventOptions = { - prefix: string; -}; - -export default class Event { - private readonly [editorSymbol]: InnerEditor; - private readonly options: EventOptions; - - // TODO: - /** - * 内核触发的事件名 - */ - readonly names = []; - - constructor(editor: InnerEditor, options: EventOptions) { - this[editorSymbol] = editor; - this.options = options; - if (!this.options.prefix) { - logger.warn('prefix is required while initializing Event'); - } - } - - /** - * 监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - on(event: string, listener: (...args: unknown[]) => void) { - if (event.startsWith('designer')) { - logger.warn('designer events are disabled'); - return; - } - this[editorSymbol].on(event, listener); - } - - /** - * 取消监听事件 - * @param event 事件名称 - * @param listener 事件回调 - */ - off(event: string, listener: (...args: unknown[]) => void) { - this[editorSymbol].off(event, listener); - } - - /** - * 触发事件 - * @param event 事件名称 - * @param args 事件参数 - * @returns - */ - emit(event: string, ...args: unknown[]) { - if (!this.options.prefix) { - logger.warn('Event#emit has been forbidden while prefix is not specified'); - return; - } - this[editorSymbol].emit(`${this.options.prefix}:${event}`, ...args); - } - - /** - * DO NOT USE if u fully understand what this method does. - * @param event - * @param args - */ - __internalEmit__(event: string, ...args: unknown[]) { - this[editorSymbol].emit(event, ...args); - } -} - -export function getEvent(editor: InnerEditor, options: any = { prefix: 'common' }) { - return new Event(editor, options); -} diff --git a/packages/shell/src/history.ts b/packages/shell/src/history.ts deleted file mode 100644 index debc0ccef9..0000000000 --- a/packages/shell/src/history.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { History as InnerHistory, DocumentModel as InnerDocumentModel } from '@alilc/lowcode-designer'; -import { historySymbol } from './symbols'; - -export default class History { - private readonly [historySymbol]: InnerHistory; - - constructor(history: InnerHistory) { - this[historySymbol] = history; - } - - /** - * 历史记录跳转到指定位置 - * @param cursor - */ - go(cursor: number) { - this[historySymbol].go(cursor); - } - - /** - * 历史记录后退 - */ - back() { - this[historySymbol].back(); - } - - /** - * 历史记录前进 - */ - forward() { - this[historySymbol].forward(); - } - - /** - * 保存当前状态 - */ - savePoint() { - this[historySymbol].savePoint(); - } - - /** - * 当前是否是「保存点」,即是否有状态变更但未保存 - * @returns - */ - isSavePoint() { - return this[historySymbol].isSavePoint(); - } - - /** - * 获取 state,判断当前是否为「可回退」、「可前进」的状态 - * @returns - */ - getState() { - return this[historySymbol].getState(); - } - - /** - * 监听 state 变更事件 - * @param func - * @returns - */ - onChangeState(func: () => any) { - return this[historySymbol].onStateChange(func); - } - - /** - * 监听历史记录游标位置变更事件 - * @param func - * @returns - */ - onChangeCursor(func: () => any) { - return this[historySymbol].onCursor(func); - } -} diff --git a/packages/shell/src/hotkey.ts b/packages/shell/src/hotkey.ts deleted file mode 100644 index a60be7dd9b..0000000000 --- a/packages/shell/src/hotkey.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { hotkey, HotkeyCallback } from '@alilc/lowcode-editor-core'; -import { Disposable } from '@alilc/lowcode-types'; - -export default class Hotkey { - get callbacks() { - return hotkey.callBacks; - } - /** - * @deprecated - */ - get callBacks() { - return this.callbacks; - } - /** - * 绑定快捷键 - * @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等 - * @param callback 回调函数 - * @param action - * @returns - */ - bind(combos: string[] | string, callback: HotkeyCallback, action?: string): Disposable { - hotkey.bind(combos, callback, action); - return () => { - hotkey.unbind(combos, callback, action); - }; - } -} \ No newline at end of file diff --git a/packages/shell/src/index.ts b/packages/shell/src/index.ts index 07a0c83a75..fb1e7228f3 100644 --- a/packages/shell/src/index.ts +++ b/packages/shell/src/index.ts @@ -1,20 +1,37 @@ -import Detecting from './detecting'; -// import Dragon from './dragon'; -import DocumentModel from './document-model'; -import Event, { getEvent } from './event'; -import History from './history'; -import Material from './material'; -import Node from './node'; -import NodeChildren from './node-children'; -import Project from './project'; -import Prop from './prop'; -import Selection from './selection'; -import Setters from './setters'; -import Hotkey from './hotkey'; -import Skeleton from './skeleton'; -import Dragon from './dragon'; -import SettingPropEntry from './setting-prop-entry'; -import SettingTopEntry from './setting-top-entry'; +import { + Detecting, + DocumentModel, + History, + Node, + NodeChildren, + Prop, + Selection, + Dragon, + SettingTopEntry, + Clipboard, + SettingField, + Window, + SkeletonItem, +} from './model'; +import { + Project, + Material, + Logger, + Plugins, + Skeleton, + Setters, + Hotkey, + Common, + getEvent, + Event, + Canvas, + Workspace, + SimulatorHost, + Config, + CommonUI, + Command, +} from './api'; + export * from './symbols'; /** @@ -27,7 +44,6 @@ export * from './symbols'; export { DocumentModel, Detecting, - // Dragon, Event, History, Material, @@ -38,9 +54,22 @@ export { Selection, Setters, Hotkey, + Window, Skeleton, - SettingPropEntry, + SettingField as SettingPropEntry, SettingTopEntry, Dragon, + Common, getEvent, -}; \ No newline at end of file + Plugins, + Logger, + Canvas, + Workspace, + Clipboard, + SimulatorHost, + Config, + SettingField, + SkeletonItem, + CommonUI, + Command, +}; diff --git a/packages/shell/src/locate-event.ts b/packages/shell/src/locate-event.ts deleted file mode 100644 index 01c97edaec..0000000000 --- a/packages/shell/src/locate-event.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { LocateEvent as InnerLocateEvent } from '@alilc/lowcode-designer'; -import { locateEventSymbol } from './symbols'; -import DragObject from './drag-object'; - -export default class LocateEvent { - private readonly [locateEventSymbol]: InnerLocateEvent; - - constructor(locateEvent: InnerLocateEvent) { - this[locateEventSymbol] = locateEvent; - } - - static create(locateEvent: InnerLocateEvent) { - if (!locateEvent) return null; - return new LocateEvent(locateEvent); - } - - get type() { - return this[locateEventSymbol].type; - } - - get globalX() { - return this[locateEventSymbol].globalX; - } - - get globalY() { - return this[locateEventSymbol].globalY; - } - - get originalEvent() { - return this[locateEventSymbol].originalEvent; - } - - get target() { - return this[locateEventSymbol].target; - } - - get canvasX() { - return this[locateEventSymbol].canvasX; - } - - get canvasY() { - return this[locateEventSymbol].canvasY; - } - - get dragObject() { - return DragObject.create(this[locateEventSymbol].dragObject); - } -} \ No newline at end of file diff --git a/packages/shell/src/material.ts b/packages/shell/src/material.ts deleted file mode 100644 index 46b25beab5..0000000000 --- a/packages/shell/src/material.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { Editor } from '@alilc/lowcode-editor-core'; -import { - Designer, - registerMetadataTransducer, - MetadataTransducer, - getRegisteredMetadataTransducers, - addBuiltinComponentAction, - removeBuiltinComponentAction, - modifyBuiltinComponentAction, - isComponentMeta, -} from '@alilc/lowcode-designer'; -import { AssetsJson } from '@alilc/lowcode-utils'; -import { ComponentAction, ComponentMetadata } from '@alilc/lowcode-types'; -import { editorSymbol, designerSymbol } from './symbols'; -import ComponentMeta from './component-meta'; - -export default class Material { - private readonly [editorSymbol]: Editor; - private readonly [designerSymbol]: Designer; - - constructor(editor: Editor) { - this[editorSymbol] = editor; - this[designerSymbol] = editor.get('designer')!; - } - - /** - * 获取组件 map 结构 - */ - get componentsMap() { - return this[designerSymbol].componentsMap; - } - - /** - * 设置「资产包」结构 - * @param assets - * @returns - */ - async setAssets(assets: AssetsJson) { - return await this[editorSymbol].setAssets(assets); - } - - /** - * 获取「资产包」结构 - * @returns - */ - getAssets() { - return this[editorSymbol].get('assets'); - } - - /** - * 加载增量的「资产包」结构,该增量包会与原有的合并 - * @param incrementalAssets - * @returns - */ - loadIncrementalAssets(incrementalAssets: AssetsJson) { - return this[designerSymbol].loadIncrementalAssets(incrementalAssets); - } - - /** - * 注册物料元数据管道函数 - * @param transducer - * @param level - * @param id - */ - registerMetadataTransducer( - transducer: MetadataTransducer, - level?: number, - id?: string | undefined, - ) { - registerMetadataTransducer(transducer, level, id); - } - - /** - * 获取所有物料元数据管道函数 - * @returns - */ - getRegisteredMetadataTransducers() { - return getRegisteredMetadataTransducers(); - } - - /** - * 获取指定名称的物料元数据 - * @param componentName - * @returns - */ - getComponentMeta(componentName: string) { - return ComponentMeta.create(this[designerSymbol].getComponentMeta(componentName)); - } - - /** - * create an instance of ComponentMeta by given metadata - * @param metadata - * @returns - */ - createComponentMeta(metadata: ComponentMetadata) { - return ComponentMeta.create(this[designerSymbol].createComponentMeta(metadata)); - } - - /** - * test if the given object is a ComponentMeta instance or not - * @param obj - * @returns - */ - isComponentMeta(obj: any) { - return isComponentMeta(obj); - } - - /** - * 获取所有已注册的物料元数据 - * @returns - */ - getComponentMetasMap() { - const map = new Map<string, ComponentMeta>(); - const originalMap = this[designerSymbol].getComponentMetasMap(); - for (let componentName of originalMap.keys()) { - map.set(componentName, this.getComponentMeta(componentName)!); - } - return map; - } - - /** - * 在设计器辅助层增加一个扩展 action - * @param action - */ - addBuiltinComponentAction(action: ComponentAction) { - addBuiltinComponentAction(action); - } - - /** - * 移除设计器辅助层的指定 action - * @param name - */ - removeBuiltinComponentAction(name: string) { - removeBuiltinComponentAction(name); - } - - /** - * 修改已有的设计器辅助层的指定 action - * @param actionName - * @param handle - */ - modifyBuiltinComponentAction(actionName: string, handle: (action: ComponentAction) => void) { - modifyBuiltinComponentAction(actionName, handle); - } - - /** - * 监听 assets 变化的事件 - * @param fn - */ - onChangeAssets(fn: () => void) { - // 设置 assets,经过 setAssets 赋值 - this[editorSymbol].onGot('assets', fn); - // 增量设置 assets,经过 loadIncrementalAssets 赋值 - this[editorSymbol].on('designer.incrementalAssetsReady', fn); - } -} diff --git a/packages/shell/src/modal-nodes-manager.ts b/packages/shell/src/modal-nodes-manager.ts deleted file mode 100644 index 739ca9406d..0000000000 --- a/packages/shell/src/modal-nodes-manager.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { ModalNodesManager as InnerModalNodesManager, Node as InnerNode } from '@alilc/lowcode-designer'; -import { NodeSchema, NodeData, TransformStage } from '@alilc/lowcode-types'; -import Node from './node'; -import { nodeSymbol, modalNodesManagerSymbol } from './symbols'; - -export default class ModalNodesManager { - private readonly [modalNodesManagerSymbol]: InnerModalNodesManager; - - constructor(modalNodesManager: InnerModalNodesManager) { - this[modalNodesManagerSymbol] = modalNodesManager; - } - - static create(modalNodesManager: InnerModalNodesManager | null) { - if (!modalNodesManager) return null; - return new ModalNodesManager(modalNodesManager); - } - - /** - * 设置模态节点,触发内部事件 - */ - setNodes() { - this[modalNodesManagerSymbol].setNodes(); - } - - /** - * 获取模态节点(们) - * @returns - */ - getModalNodes() { - return this[modalNodesManagerSymbol].getModalNodes().map((node) => Node.create(node)); - } - - /** - * 获取当前可见的模态节点 - * @returns - */ - getVisibleModalNode() { - return Node.create(this[modalNodesManagerSymbol].getVisibleModalNode()); - } - - /** - * 隐藏模态节点(们) - */ - hideModalNodes() { - this[modalNodesManagerSymbol].hideModalNodes(); - } - - /** - * 设置指定节点为可见态 - * @param node Node - */ - setVisible(node: Node) { - this[modalNodesManagerSymbol].setVisible(node[nodeSymbol]); - } - - /** - * 设置指定节点为不可见态 - * @param node Node - */ - setInvisible(node: Node) { - this[modalNodesManagerSymbol].setInvisible(node[nodeSymbol]); - } -} \ No newline at end of file diff --git a/packages/shell/src/model/active-tracker.ts b/packages/shell/src/model/active-tracker.ts new file mode 100644 index 0000000000..32d4c04eb9 --- /dev/null +++ b/packages/shell/src/model/active-tracker.ts @@ -0,0 +1,50 @@ +import { IPublicModelActiveTracker, IPublicModelNode, IPublicTypeActiveTarget } from '@alilc/lowcode-types'; +import { IActiveTracker as InnerActiveTracker, ActiveTarget } from '@alilc/lowcode-designer'; +import { Node as ShellNode } from './node'; +import { nodeSymbol } from '../symbols'; + +const activeTrackerSymbol = Symbol('activeTracker'); + +export class ActiveTracker implements IPublicModelActiveTracker { + private readonly [activeTrackerSymbol]: InnerActiveTracker; + + constructor(innerTracker: InnerActiveTracker) { + this[activeTrackerSymbol] = innerTracker; + } + + get target() { + const _target = this[activeTrackerSymbol]._target; + + if (!_target) { + return null; + } + + const { node: innerNode, detail, instance } = _target; + const publicNode = ShellNode.create(innerNode); + return { + node: publicNode!, + detail, + instance, + }; + } + + onChange(fn: (target: IPublicTypeActiveTarget) => void): () => void { + if (!fn) { + return () => {}; + } + return this[activeTrackerSymbol].onChange((t: ActiveTarget) => { + const { node: innerNode, detail, instance } = t; + const publicNode = ShellNode.create(innerNode); + const publicActiveTarget = { + node: publicNode!, + detail, + instance, + }; + fn(publicActiveTarget); + }); + } + + track(node: IPublicModelNode) { + this[activeTrackerSymbol].track((node as any)[nodeSymbol]); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/clipboard.ts b/packages/shell/src/model/clipboard.ts new file mode 100644 index 0000000000..9c4b309450 --- /dev/null +++ b/packages/shell/src/model/clipboard.ts @@ -0,0 +1,22 @@ +import { IPublicModelClipboard } from '@alilc/lowcode-types'; +import { clipboardSymbol } from '../symbols'; +import { IClipboard, clipboard } from '@alilc/lowcode-designer'; + +export class Clipboard implements IPublicModelClipboard { + private readonly [clipboardSymbol]: IClipboard; + + constructor() { + this[clipboardSymbol] = clipboard; + } + + setData(data: any): void { + this[clipboardSymbol].setData(data); + } + + waitPasteData( + keyboardEvent: KeyboardEvent, + cb: (data: any, clipboardEvent: ClipboardEvent) => void, + ): void { + this[clipboardSymbol].waitPasteData(keyboardEvent, cb); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/component-meta.ts b/packages/shell/src/model/component-meta.ts new file mode 100644 index 0000000000..448f0584ee --- /dev/null +++ b/packages/shell/src/model/component-meta.ts @@ -0,0 +1,146 @@ +import { + IComponentMeta as InnerComponentMeta, + INode, +} from '@alilc/lowcode-designer'; +import { IPublicTypeNodeData, IPublicTypeNodeSchema, IPublicModelComponentMeta, IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeNpmInfo, IPublicTypeTransformedComponentMetadata, IPublicModelNode, IPublicTypeAdvanced, IPublicTypeFieldConfig } from '@alilc/lowcode-types'; +import { componentMetaSymbol, nodeSymbol } from '../symbols'; +import { ReactElement } from 'react'; + +export class ComponentMeta implements IPublicModelComponentMeta { + private readonly [componentMetaSymbol]: InnerComponentMeta; + + isComponentMeta = true; + + constructor(componentMeta: InnerComponentMeta) { + this[componentMetaSymbol] = componentMeta; + } + + static create(componentMeta: InnerComponentMeta | null): IPublicModelComponentMeta | null { + if (!componentMeta) { + return null; + } + return new ComponentMeta(componentMeta); + } + + /** + * 组件名 + */ + get componentName(): string { + return this[componentMetaSymbol].componentName; + } + + /** + * 是否是「容器型」组件 + */ + get isContainer(): boolean { + return this[componentMetaSymbol].isContainer; + } + + /** + * 是否是最小渲染单元。 + * 当组件需要重新渲染时: + * 若为最小渲染单元,则只渲染当前组件, + * 若不为最小渲染单元,则寻找到上层最近的最小渲染单元进行重新渲染,直至根节点。 + */ + get isMinimalRenderUnit(): boolean { + return this[componentMetaSymbol].isMinimalRenderUnit; + } + + /** + * 是否为「模态框」组件 + */ + get isModal(): boolean { + return this[componentMetaSymbol].isModal; + } + + /** + * 元数据配置 + */ + get configure(): IPublicTypeFieldConfig[] { + return this[componentMetaSymbol].configure; + } + + /** + * 标题 + */ + get title(): string | IPublicTypeI18nData | ReactElement { + return this[componentMetaSymbol].title; + } + + /** + * 图标 + */ + get icon(): IPublicTypeIconType { + return this[componentMetaSymbol].icon; + } + + /** + * 组件 npm 信息 + */ + get npm(): IPublicTypeNpmInfo { + return this[componentMetaSymbol].npm; + } + + /** + * @deprecated + */ + get prototype() { + return (this[componentMetaSymbol] as any).prototype; + } + + get availableActions(): any { + return this[componentMetaSymbol].availableActions; + } + + get advanced(): IPublicTypeAdvanced { + return this[componentMetaSymbol].advanced; + } + + /** + * 设置 npm 信息 + * @param npm + */ + setNpm(npm: IPublicTypeNpmInfo): void { + this[componentMetaSymbol].setNpm(npm); + } + + /** + * 获取元数据 + * @returns + */ + getMetadata(): IPublicTypeTransformedComponentMetadata { + return this[componentMetaSymbol].getMetadata(); + } + + /** + * check if the current node could be placed in parent node + * @param my + * @param parent + * @returns + */ + checkNestingUp(my: IPublicModelNode | IPublicTypeNodeData, parent: INode): boolean { + const curNode = (my as any).isNode ? (my as any)[nodeSymbol] : my; + return this[componentMetaSymbol].checkNestingUp(curNode as any, parent); + } + + /** + * check if the target node(s) could be placed in current node + * @param my + * @param parent + * @returns + */ + checkNestingDown( + my: IPublicModelNode | IPublicTypeNodeData, + target: IPublicTypeNodeSchema | IPublicModelNode | IPublicTypeNodeSchema[], + ) { + const curNode = (my as any)?.isNode ? (my as any)[nodeSymbol] : my; + return this[componentMetaSymbol].checkNestingDown( + curNode as any, + (target as any)[nodeSymbol] || target, + ); + } + + refreshMetadata(): void { + this[componentMetaSymbol].refreshMetadata(); + } +} diff --git a/packages/shell/src/model/condition-group.ts b/packages/shell/src/model/condition-group.ts new file mode 100644 index 0000000000..e2dd316edc --- /dev/null +++ b/packages/shell/src/model/condition-group.ts @@ -0,0 +1,42 @@ +import type { IExclusiveGroup } from '@alilc/lowcode-designer'; +import { IPublicModelExclusiveGroup, IPublicModelNode } from '@alilc/lowcode-types'; +import { conditionGroupSymbol, nodeSymbol } from '../symbols'; +import { Node } from './node'; + +export class ConditionGroup implements IPublicModelExclusiveGroup { + private [conditionGroupSymbol]: IExclusiveGroup | null; + + constructor(conditionGroup: IExclusiveGroup | null) { + this[conditionGroupSymbol] = conditionGroup; + } + + get id() { + return this[conditionGroupSymbol]?.id; + } + + get title() { + return this[conditionGroupSymbol]?.title; + } + + get firstNode() { + return Node.create(this[conditionGroupSymbol]?.firstNode); + } + + setVisible(node: IPublicModelNode) { + this[conditionGroupSymbol]?.setVisible((node as any)[nodeSymbol] ? (node as any)[nodeSymbol] : node); + } + + static create(conditionGroup: IExclusiveGroup | null) { + if (!conditionGroup) { + return null; + } + // @ts-ignore + if (conditionGroup[conditionGroupSymbol]) { + return (conditionGroup as any)[conditionGroupSymbol]; + } + const shellConditionGroup = new ConditionGroup(conditionGroup); + // @ts-ignore + shellConditionGroup[conditionGroupSymbol] = shellConditionGroup; + return shellConditionGroup; + } +} diff --git a/packages/shell/src/model/detecting.ts b/packages/shell/src/model/detecting.ts new file mode 100644 index 0000000000..7ce0fe1e5c --- /dev/null +++ b/packages/shell/src/model/detecting.ts @@ -0,0 +1,63 @@ +import { Node as ShellNode } from './node'; +import { + Detecting as InnerDetecting, + IDocumentModel as InnerDocumentModel, + INode as InnerNode, +} from '@alilc/lowcode-designer'; +import { documentSymbol, detectingSymbol } from '../symbols'; +import { IPublicModelDetecting, IPublicModelNode, IPublicTypeDisposable } from '@alilc/lowcode-types'; + +export class Detecting implements IPublicModelDetecting { + private readonly [documentSymbol]: InnerDocumentModel; + private readonly [detectingSymbol]: InnerDetecting; + + constructor(document: InnerDocumentModel) { + this[documentSymbol] = document; + this[detectingSymbol] = document.designer?.detecting; + } + + /** + * 控制大纲树 hover 时是否出现悬停效果 + */ + get enable(): boolean { + return this[detectingSymbol].enable; + } + + /** + * 当前 hover 的节点 + */ + get current() { + return ShellNode.create(this[detectingSymbol].current); + } + + /** + * hover 指定节点 + * @param id 节点 id + */ + capture(id: string) { + this[detectingSymbol].capture(this[documentSymbol].getNode(id)); + } + + /** + * hover 离开指定节点 + * @param id 节点 id + */ + release(id: string) { + this[detectingSymbol].release(this[documentSymbol].getNode(id)); + } + + /** + * 清空 hover 态 + */ + leave() { + this[detectingSymbol].leave(this[documentSymbol]); + } + + onDetectingChange(fn: (node: IPublicModelNode | null) => void): IPublicTypeDisposable { + const innerFn = (innerNode: InnerNode) => { + const shellNode = ShellNode.create(innerNode); + fn(shellNode); + }; + return this[detectingSymbol].onDetectingChange(innerFn); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/document-model.ts b/packages/shell/src/model/document-model.ts new file mode 100644 index 0000000000..bd0ccaf75e --- /dev/null +++ b/packages/shell/src/model/document-model.ts @@ -0,0 +1,381 @@ +import { + IDocumentModel as InnerDocumentModel, + INode as InnerNode, +} from '@alilc/lowcode-designer'; +import { + IPublicEnumTransformStage, + IPublicTypeRootSchema, + GlobalEvent, + IPublicModelDocumentModel, + IPublicTypeOnChangeOptions, + IPublicTypeDragNodeObject, + IPublicTypeDragNodeDataObject, + IPublicModelNode, + IPublicModelSelection, + IPublicModelDetecting, + IPublicModelHistory, + IPublicApiProject, + IPublicModelModalNodesManager, + IPublicTypePropChangeOptions, + IPublicModelDropLocation, + IPublicApiCanvas, + IPublicTypeDisposable, + IPublicModelEditor, + IPublicTypeNodeSchema, +} from '@alilc/lowcode-types'; +import { isDragNodeObject } from '@alilc/lowcode-utils'; +import { Node as ShellNode } from './node'; +import { Selection as ShellSelection } from './selection'; +import { Detecting as ShellDetecting } from './detecting'; +import { History as ShellHistory } from './history'; +import { DropLocation as ShellDropLocation } from './drop-location'; +import { Project as ShellProject, Canvas as ShellCanvas } from '../api'; +import { Prop as ShellProp } from './prop'; +import { ModalNodesManager } from './modal-nodes-manager'; +import { documentSymbol, editorSymbol, nodeSymbol } from '../symbols'; + +const shellDocSymbol = Symbol('shellDocSymbol'); + +export class DocumentModel implements IPublicModelDocumentModel { + private readonly [documentSymbol]: InnerDocumentModel; + private readonly [editorSymbol]: IPublicModelEditor; + private _focusNode: IPublicModelNode | null; + selection: IPublicModelSelection; + detecting: IPublicModelDetecting; + history: IPublicModelHistory; + + /** + * @deprecated use canvas API instead + */ + canvas: IPublicApiCanvas; + + constructor(document: InnerDocumentModel) { + this[documentSymbol] = document; + this[editorSymbol] = document.designer?.editor as IPublicModelEditor; + this.selection = new ShellSelection(document); + this.detecting = new ShellDetecting(document); + this.history = new ShellHistory(document); + this.canvas = new ShellCanvas(this[editorSymbol]); + + this._focusNode = ShellNode.create(this[documentSymbol].focusNode); + } + + static create(document: InnerDocumentModel | undefined | null): IPublicModelDocumentModel | null { + if (!document) { + return null; + } + // @ts-ignore 直接返回已挂载的 shell doc 实例 + if (document[shellDocSymbol]) { + return (document as any)[shellDocSymbol]; + } + const shellDoc = new DocumentModel(document); + // @ts-ignore 直接返回已挂载的 shell doc 实例 + document[shellDocSymbol] = shellDoc; + return shellDoc; + } + + /** + * id + */ + get id(): string { + return this[documentSymbol].id; + } + + set id(id) { + this[documentSymbol].id = id; + } + + /** + * 获取当前文档所属的 project + * @returns + */ + get project(): IPublicApiProject { + return ShellProject.create(this[documentSymbol].project, true); + } + + /** + * 获取文档的根节点 + * root node of this documentModel + * @returns + */ + get root(): IPublicModelNode | null { + return ShellNode.create(this[documentSymbol].rootNode); + } + + get focusNode(): IPublicModelNode | null { + return this._focusNode || this.root; + } + + set focusNode(node: IPublicModelNode | null) { + this._focusNode = node; + this[editorSymbol].eventBus.emit( + 'shell.document.focusNodeChanged', + { document: this, focusNode: node }, + ); + } + + /** + * 获取文档下所有节点 Map, key 为 nodeId + * get map of all nodes , using node.id as key + */ + get nodesMap(): Map<string, IPublicModelNode> { + const map = new Map<string, IPublicModelNode>(); + for (let id of this[documentSymbol].nodesMap.keys()) { + map.set(id, this.getNodeById(id)!); + } + return map; + } + + /** + * 模态节点管理 + */ + get modalNodesManager(): IPublicModelModalNodesManager | null { + return ModalNodesManager.create(this[documentSymbol].modalNodesManager); + } + + get dropLocation(): IPublicModelDropLocation | null { + return ShellDropLocation.create(this[documentSymbol].dropLocation); + } + + set dropLocation(loc: IPublicModelDropLocation | null) { + this[documentSymbol].dropLocation = loc; + } + + /** + * 根据 nodeId 返回 Node 实例 + * get node instance by nodeId + * @param {string} nodeId + */ + getNodeById(nodeId: string): IPublicModelNode | null { + return ShellNode.create(this[documentSymbol].getNode(nodeId)); + } + + /** + * 导入 schema + * @param schema + */ + importSchema(schema: IPublicTypeRootSchema): void { + this[documentSymbol].import(schema); + this[editorSymbol].eventBus.emit('shell.document.importSchema', schema); + } + + /** + * 导出 schema + * @param stage + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render): IPublicTypeRootSchema | undefined { + return this[documentSymbol].export(stage); + } + + /** + * 插入节点 + * @param parent + * @param thing + * @param at + * @param copy + * @returns + */ + insertNode( + parent: IPublicModelNode, + thing: IPublicModelNode, + at?: number | null | undefined, + copy?: boolean | undefined, + ): IPublicModelNode | null { + const node = this[documentSymbol].insertNode( + (parent as any)[nodeSymbol] ? (parent as any)[nodeSymbol] : parent, + (thing as any)?.[nodeSymbol] ? (thing as any)[nodeSymbol] : thing, + at, + copy, + ); + return ShellNode.create(node); + } + + /** + * 创建一个节点 + * @param data + * @returns + */ + createNode<IPublicModelNode>(data: IPublicTypeNodeSchema): IPublicModelNode | null { + return ShellNode.create(this[documentSymbol].createNode(data)); + } + + /** + * 移除指定节点/节点id + * @param idOrNode + */ + removeNode(idOrNode: string | IPublicModelNode): void { + this[documentSymbol].removeNode(idOrNode as any); + } + + /** + * componentsMap of documentModel + * @param extraComps + * @returns + */ + getComponentsMap(extraComps?: string[]): any { + return this[documentSymbol].getComponentsMap(extraComps); + } + + /** + * 检查拖拽放置的目标节点是否可以放置该拖拽对象 + * @param dropTarget 拖拽放置的目标节点 + * @param dragObject 拖拽的对象 + * @returns boolean 是否可以放置 + */ + checkNesting( + dropTarget: IPublicModelNode, + dragObject: IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject, + ): boolean { + let innerDragObject = dragObject; + if (isDragNodeObject(dragObject)) { + innerDragObject.nodes = innerDragObject.nodes?.map( + (node: IPublicModelNode) => ((node as any)[nodeSymbol] || node), + ); + } + return this[documentSymbol].checkNesting( + ((dropTarget as any)[nodeSymbol] || dropTarget) as any, + innerDragObject as any, + ); + } + + /** + * 当前 document 新增节点事件 + */ + onAddNode(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { + return this[documentSymbol].onNodeCreate((node: InnerNode) => { + fn(ShellNode.create(node)!); + }); + } + + /** + * 当前 document 新增节点事件,此时节点已经挂载到 document 上 + */ + onMountNode(fn: (payload: { node: IPublicModelNode }) => void): IPublicTypeDisposable { + return this[documentSymbol].onMountNode(({ + node, + }) => { + fn({ node: ShellNode.create(node)! }); + }); + } + + /** + * 当前 document 删除节点事件 + */ + onRemoveNode(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { + return this[documentSymbol].onNodeDestroy((node: InnerNode) => { + fn(ShellNode.create(node)!); + }); + } + + /** + * 当前 document 的 hover 变更事件 + */ + onChangeDetecting(fn: (node: IPublicModelNode) => void): IPublicTypeDisposable { + return this[documentSymbol].designer.detecting.onDetectingChange((node: InnerNode) => { + fn(ShellNode.create(node)!); + }); + } + + /** + * 当前 document 的选中变更事件 + */ + onChangeSelection(fn: (ids: string[]) => void): IPublicTypeDisposable { + return this[documentSymbol].selection.onSelectionChange((ids: string[]) => { + fn(ids); + }); + } + + /** + * 当前 document 的节点显隐状态变更事件 + * @param fn + */ + onChangeNodeVisible(fn: (node: IPublicModelNode, visible: boolean) => void): IPublicTypeDisposable { + return this[documentSymbol].onChangeNodeVisible((node: InnerNode, visible: boolean) => { + fn(ShellNode.create(node)!, visible); + }); + } + + /** + * 当前 document 的节点 children 变更事件 + * @param fn + */ + onChangeNodeChildren(fn: (info: IPublicTypeOnChangeOptions) => void): IPublicTypeDisposable { + return this[documentSymbol].onChangeNodeChildren((info?: IPublicTypeOnChangeOptions<InnerNode>) => { + if (!info) { + return; + } + fn({ + type: info.type, + node: ShellNode.create(info.node)!, + }); + }); + } + + /** + * 当前 document 节点属性修改事件 + * @param fn + */ + onChangeNodeProp(fn: (info: IPublicTypePropChangeOptions) => void): IPublicTypeDisposable { + const callback = (info: GlobalEvent.Node.Prop.ChangeOptions) => { + fn({ + key: info.key, + oldValue: info.oldValue, + newValue: info.newValue, + prop: ShellProp.create(info.prop)!, + node: ShellNode.create(info.node as any)!, + }); + }; + this[editorSymbol].on( + GlobalEvent.Node.Prop.InnerChange, + callback, + ); + + return () => { + this[editorSymbol].off( + GlobalEvent.Node.Prop.InnerChange, + callback, + ); + }; + } + + /** + * import schema event + * @param fn + */ + onImportSchema(fn: (schema: IPublicTypeRootSchema) => void): IPublicTypeDisposable { + return this[editorSymbol].eventBus.on('shell.document.importSchema', fn as any); + } + + isDetectingNode(node: IPublicModelNode): boolean { + return this.detecting.current === node; + } + + onFocusNodeChanged( + fn: (doc: IPublicModelDocumentModel, focusNode: IPublicModelNode) => void, + ): IPublicTypeDisposable { + if (!fn) { + return () => {}; + } + return this[editorSymbol].eventBus.on( + 'shell.document.focusNodeChanged', + (payload) => { + const { document, focusNode } = payload; + fn(document, focusNode); + }, + ); + } + + onDropLocationChanged(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable { + if (!fn) { + return () => {}; + } + return this[editorSymbol].eventBus.on( + 'document.dropLocation.changed', + (payload) => { + const { document } = payload; + fn(document); + }, + ); + } +} diff --git a/packages/shell/src/model/drag-object.ts b/packages/shell/src/model/drag-object.ts new file mode 100644 index 0000000000..064680fdee --- /dev/null +++ b/packages/shell/src/model/drag-object.ts @@ -0,0 +1,34 @@ +import { dragObjectSymbol } from '../symbols'; +import { IPublicModelDragObject, IPublicModelDragObject as InnerDragObject, IPublicTypeDragNodeDataObject, IPublicTypeNodeSchema } from '@alilc/lowcode-types'; +import { Node } from './node'; + +export class DragObject implements IPublicModelDragObject { + private readonly [dragObjectSymbol]: InnerDragObject; + + constructor(dragObject: InnerDragObject) { + this[dragObjectSymbol] = dragObject; + } + + static create(dragObject: InnerDragObject | null): IPublicModelDragObject | null { + if (!dragObject) { + return null; + } + return new DragObject(dragObject); + } + + get type() { + return this[dragObjectSymbol].type; + } + + get nodes() { + const { nodes } = this[dragObjectSymbol]; + if (!nodes) { + return null; + } + return nodes.map(Node.create); + } + + get data(): IPublicTypeNodeSchema | IPublicTypeNodeSchema[] { + return (this[dragObjectSymbol] as IPublicTypeDragNodeDataObject).data; + } +} \ No newline at end of file diff --git a/packages/shell/src/model/dragon.ts b/packages/shell/src/model/dragon.ts new file mode 100644 index 0000000000..7f2492e7ea --- /dev/null +++ b/packages/shell/src/model/dragon.ts @@ -0,0 +1,130 @@ +import { + IDragon, + ILocateEvent as InnerLocateEvent, + INode, +} from '@alilc/lowcode-designer'; +import { dragonSymbol, nodeSymbol } from '../symbols'; +import LocateEvent from './locate-event'; +import { DragObject } from './drag-object'; +import { globalContext } from '@alilc/lowcode-editor-core'; +import { + IPublicModelDragon, + IPublicModelLocateEvent, + IPublicModelDragObject, + IPublicTypeDragNodeDataObject, + IPublicModelNode, + IPublicTypeDragObject, +} from '@alilc/lowcode-types'; + +export const innerDragonSymbol = Symbol('innerDragonSymbol'); + +export class Dragon implements IPublicModelDragon { + private readonly [innerDragonSymbol]: IDragon; + + constructor(innerDragon: IDragon, readonly workspaceMode: boolean) { + this[innerDragonSymbol] = innerDragon; + } + + get [dragonSymbol](): IDragon { + if (this.workspaceMode) { + return this[innerDragonSymbol]; + } + const workspace = globalContext.get('workspace'); + let editor = globalContext.get('editor'); + + if (workspace.isActive) { + editor = workspace.window.editor; + } + + const designer = editor.get('designer'); + return designer.dragon; + } + + static create( + dragon: IDragon | null, + workspaceMode: boolean, + ): IPublicModelDragon | null { + if (!dragon) { + return null; + } + return new Dragon(dragon, workspaceMode); + } + + /** + * is dragging or not + */ + get dragging(): boolean { + return this[dragonSymbol].dragging; + } + + /** + * 绑定 dragstart 事件 + * @param func + * @returns + */ + onDragstart(func: (e: IPublicModelLocateEvent) => any): () => void { + return this[dragonSymbol].onDragstart((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); + } + + /** + * 绑定 drag 事件 + * @param func + * @returns + */ + onDrag(func: (e: IPublicModelLocateEvent) => any): () => void { + return this[dragonSymbol].onDrag((e: InnerLocateEvent) => func(LocateEvent.create(e)!)); + } + + /** + * 绑定 dragend 事件 + * @param func + * @returns + */ + onDragend(func: (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => any): () => void { + return this[dragonSymbol].onDragend( + (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => { + const dragObject = DragObject.create(o.dragObject); + const { copy } = o; + return func({ dragObject: dragObject!, copy }); + }, + ); + } + + /** + * 设置拖拽监听的区域 shell,以及自定义拖拽转换函数 boost + * @param shell 拖拽监听的区域 + * @param boost 拖拽转换函数 + */ + from(shell: Element, boost: (e: MouseEvent) => IPublicTypeDragNodeDataObject | null): any { + return this[dragonSymbol].from(shell, boost); + } + + /** + * boost your dragObject for dragging(flying) 发射拖拽对象 + * + * @param dragObject 拖拽对象 + * @param boostEvent 拖拽初始时事件 + */ + boost(dragObject: IPublicTypeDragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: IPublicModelNode & { + [nodeSymbol]: INode; + }): void { + return this[dragonSymbol].boost({ + ...dragObject, + nodes: dragObject.nodes.map((node: any) => node[nodeSymbol]), + }, boostEvent, fromRglNode?.[nodeSymbol]); + } + + /** + * 添加投放感应区 + */ + addSensor(sensor: any): void { + return this[dragonSymbol].addSensor(sensor); + } + + /** + * 移除投放感应 + */ + removeSensor(sensor: any): void { + return this[dragonSymbol].removeSensor(sensor); + } +} diff --git a/packages/shell/src/model/drop-location.ts b/packages/shell/src/model/drop-location.ts new file mode 100644 index 0000000000..f38e74a07a --- /dev/null +++ b/packages/shell/src/model/drop-location.ts @@ -0,0 +1,37 @@ +import { + IDropLocation as InnerDropLocation, +} from '@alilc/lowcode-designer'; +import { dropLocationSymbol } from '../symbols'; +import { Node as ShellNode } from './node'; +import { IPublicModelDropLocation, IPublicTypeLocationDetail, IPublicModelLocateEvent } from '@alilc/lowcode-types'; + +export class DropLocation implements IPublicModelDropLocation { + private readonly [dropLocationSymbol]: InnerDropLocation; + + constructor(dropLocation: InnerDropLocation) { + this[dropLocationSymbol] = dropLocation; + } + + static create(dropLocation: InnerDropLocation | null): IPublicModelDropLocation | null { + if (!dropLocation) { + return null; + } + return new DropLocation(dropLocation); + } + + get target() { + return ShellNode.create(this[dropLocationSymbol].target); + } + + get detail(): IPublicTypeLocationDetail { + return this[dropLocationSymbol].detail; + } + + get event(): IPublicModelLocateEvent { + return this[dropLocationSymbol].event; + } + + clone(event: IPublicModelLocateEvent): IPublicModelDropLocation { + return new DropLocation(this[dropLocationSymbol].clone(event)); + } +} diff --git a/packages/shell/src/model/editor-view.ts b/packages/shell/src/model/editor-view.ts new file mode 100644 index 0000000000..92d1a57726 --- /dev/null +++ b/packages/shell/src/model/editor-view.ts @@ -0,0 +1,35 @@ +import { editorViewSymbol, pluginContextSymbol } from '../symbols'; +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; +import { IViewContext } from '@alilc/lowcode-workspace'; + +export class EditorView { + [editorViewSymbol]: IViewContext; + + [pluginContextSymbol]: IPublicModelPluginContext; + + constructor(editorView: IViewContext) { + this[editorViewSymbol] = editorView; + this[pluginContextSymbol] = this[editorViewSymbol].innerPlugins._getLowCodePluginContext({ + pluginName: editorView.editorWindow + editorView.viewName, + }); + } + + toProxy() { + return new Proxy(this, { + get(target, prop, receiver) { + if ((target[pluginContextSymbol] as any)[prop as string]) { + return Reflect.get(target[pluginContextSymbol], prop, receiver); + } + return Reflect.get(target, prop, receiver); + }, + }); + } + + get viewName() { + return this[editorViewSymbol].viewName; + } + + get viewType() { + return this[editorViewSymbol].viewType; + } +} diff --git a/packages/shell/src/model/history.ts b/packages/shell/src/model/history.ts new file mode 100644 index 0000000000..ddc567aeef --- /dev/null +++ b/packages/shell/src/model/history.ts @@ -0,0 +1,78 @@ +import type { IDocumentModel as InnerDocumentModel, IHistory as InnerHistory } from '@alilc/lowcode-designer'; +import { historySymbol, documentSymbol } from '../symbols'; +import { IPublicModelHistory, IPublicTypeDisposable } from '@alilc/lowcode-types'; + +export class History implements IPublicModelHistory { + private readonly [documentSymbol]: InnerDocumentModel; + + private get [historySymbol](): InnerHistory { + return this[documentSymbol].getHistory(); + } + + constructor(document: InnerDocumentModel) { + this[documentSymbol] = document; + } + + /** + * 历史记录跳转到指定位置 + * @param cursor + */ + go(cursor: number): void { + this[historySymbol].go(cursor); + } + + /** + * 历史记录后退 + */ + back(): void { + this[historySymbol].back(); + } + + /** + * 历史记录前进 + */ + forward(): void { + this[historySymbol].forward(); + } + + /** + * 保存当前状态 + */ + savePoint(): void { + this[historySymbol].savePoint(); + } + + /** + * 当前是否是「保存点」,即是否有状态变更但未保存 + * @returns + */ + isSavePoint(): boolean { + return this[historySymbol].isSavePoint(); + } + + /** + * 获取 state,判断当前是否为「可回退」、「可前进」的状态 + * @returns + */ + getState(): number { + return this[historySymbol].getState(); + } + + /** + * 监听 state 变更事件 + * @param func + * @returns + */ + onChangeState(func: () => any): IPublicTypeDisposable { + return this[historySymbol].onChangeState(func); + } + + /** + * 监听历史记录游标位置变更事件 + * @param func + * @returns + */ + onChangeCursor(func: () => any): IPublicTypeDisposable { + return this[historySymbol].onChangeCursor(func); + } +} diff --git a/packages/shell/src/model/index.ts b/packages/shell/src/model/index.ts new file mode 100644 index 0000000000..a15d50b549 --- /dev/null +++ b/packages/shell/src/model/index.ts @@ -0,0 +1,23 @@ +export * from './component-meta'; +export * from './detecting'; +export * from './document-model'; +export * from './drag-object'; +export * from './dragon'; +export * from './drop-location'; +export * from './history'; +export * from './locate-event'; +export * from './modal-nodes-manager'; +export * from './node-children'; +export * from './node'; +export * from './prop'; +export * from './props'; +export * from './selection'; +export * from './setting-top-entry'; +export * from './setting-field'; +export * from './resource'; +export * from './active-tracker'; +export * from './plugin-instance'; +export * from './window'; +export * from './clipboard'; +export * from './editor-view'; +export * from './skeleton-item'; diff --git a/packages/shell/src/model/locate-event.ts b/packages/shell/src/model/locate-event.ts new file mode 100644 index 0000000000..20451f9462 --- /dev/null +++ b/packages/shell/src/model/locate-event.ts @@ -0,0 +1,51 @@ +import { ILocateEvent } from '@alilc/lowcode-designer'; +import { locateEventSymbol } from '../symbols'; +import { DragObject } from './drag-object'; +import { IPublicModelLocateEvent, IPublicModelDragObject } from '@alilc/lowcode-types'; + +export default class LocateEvent implements IPublicModelLocateEvent { + private readonly [locateEventSymbol]: ILocateEvent; + + constructor(locateEvent: ILocateEvent) { + this[locateEventSymbol] = locateEvent; + } + + static create(locateEvent: ILocateEvent): IPublicModelLocateEvent | null { + if (!locateEvent) { + return null; + } + return new LocateEvent(locateEvent); + } + + get type(): string { + return this[locateEventSymbol].type; + } + + get globalX(): number { + return this[locateEventSymbol].globalX; + } + + get globalY(): number { + return this[locateEventSymbol].globalY; + } + + get originalEvent(): MouseEvent | DragEvent { + return this[locateEventSymbol].originalEvent; + } + + get target(): Element | null | undefined { + return this[locateEventSymbol].target; + } + + get canvasX(): number | undefined { + return this[locateEventSymbol].canvasX; + } + + get canvasY(): number | undefined { + return this[locateEventSymbol].canvasY; + } + + get dragObject(): IPublicModelDragObject | null { + return DragObject.create(this[locateEventSymbol].dragObject); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/modal-nodes-manager.ts b/packages/shell/src/model/modal-nodes-manager.ts new file mode 100644 index 0000000000..b1e27596f2 --- /dev/null +++ b/packages/shell/src/model/modal-nodes-manager.ts @@ -0,0 +1,76 @@ +import { + IModalNodesManager as InnerModalNodesManager, + INode as InnerNode, +} from '@alilc/lowcode-designer'; +import { IPublicModelModalNodesManager, IPublicModelNode } from '@alilc/lowcode-types'; +import { Node as ShellNode } from './node'; +import { nodeSymbol, modalNodesManagerSymbol } from '../symbols'; + +export class ModalNodesManager implements IPublicModelModalNodesManager { + private readonly [modalNodesManagerSymbol]: InnerModalNodesManager; + + constructor(modalNodesManager: InnerModalNodesManager) { + this[modalNodesManagerSymbol] = modalNodesManager; + } + + static create( + modalNodesManager: InnerModalNodesManager | null, + ): IPublicModelModalNodesManager | null { + if (!modalNodesManager) { + return null; + } + return new ModalNodesManager(modalNodesManager); + } + + /** + * 设置模态节点,触发内部事件 + */ + setNodes(): void { + this[modalNodesManagerSymbol].setNodes(); + } + + /** + * 获取模态节点(们) + */ + getModalNodes(): IPublicModelNode[] { + const innerNodes = this[modalNodesManagerSymbol].getModalNodes(); + const shellNodes: IPublicModelNode[] = []; + innerNodes?.forEach((node: InnerNode) => { + const shellNode = ShellNode.create(node); + if (shellNode) { + shellNodes.push(shellNode); + } + }); + return shellNodes; + } + + /** + * 获取当前可见的模态节点 + */ + getVisibleModalNode(): IPublicModelNode | null { + return ShellNode.create(this[modalNodesManagerSymbol].getVisibleModalNode()); + } + + /** + * 隐藏模态节点(们) + */ + hideModalNodes(): void { + this[modalNodesManagerSymbol].hideModalNodes(); + } + + /** + * 设置指定节点为可见态 + * @param node Node + */ + setVisible(node: IPublicModelNode): void { + this[modalNodesManagerSymbol].setVisible((node as any)[nodeSymbol]); + } + + /** + * 设置指定节点为不可见态 + * @param node Node + */ + setInvisible(node: IPublicModelNode): void { + this[modalNodesManagerSymbol].setInvisible((node as any)[nodeSymbol]); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/node-children.ts b/packages/shell/src/model/node-children.ts new file mode 100644 index 0000000000..b6d52e86fe --- /dev/null +++ b/packages/shell/src/model/node-children.ts @@ -0,0 +1,245 @@ +import { INode as InnerNode, INodeChildren } from '@alilc/lowcode-designer'; +import { IPublicTypeNodeData, IPublicEnumTransformStage, IPublicModelNodeChildren, IPublicModelNode } from '@alilc/lowcode-types'; +import { Node as ShellNode } from './node'; +import { nodeSymbol, nodeChildrenSymbol } from '../symbols'; + +export class NodeChildren implements IPublicModelNodeChildren { + private readonly [nodeChildrenSymbol]: INodeChildren; + + constructor(nodeChildren: INodeChildren) { + this[nodeChildrenSymbol] = nodeChildren; + } + + static create(nodeChildren: INodeChildren | null): IPublicModelNodeChildren | null { + if (!nodeChildren) { + return null; + } + return new NodeChildren(nodeChildren); + } + + /** + * 返回当前 children 实例所属的节点实例 + */ + get owner(): IPublicModelNode | null { + return ShellNode.create(this[nodeChildrenSymbol].owner); + } + + /** + * children 内的节点实例数 + */ + get size(): number { + return this[nodeChildrenSymbol].size; + } + + /** + * @deprecated + * 是否为空 + * @returns + */ + get isEmpty(): boolean { + return this[nodeChildrenSymbol].isEmptyNode; + } + + /** + * 是否为空 + * @returns + */ + get isEmptyNode(): boolean { + return this[nodeChildrenSymbol].isEmptyNode; + } + + /** + * @deprecated + * judge if it is not empty + */ + get notEmpty(): boolean { + return this[nodeChildrenSymbol].notEmptyNode; + } + + /** + * judge if it is not empty + */ + get notEmptyNode(): boolean { + return this[nodeChildrenSymbol].notEmptyNode; + } + + /** + * 删除指定节点 + * delete the node + * @param node + */ + delete(node: IPublicModelNode): boolean { + return this[nodeChildrenSymbol].delete((node as any)?.[nodeSymbol]); + } + + /** + * 插入一个节点 + * @param node 待插入节点 + * @param at 插入下标 + * @returns + */ + insert(node: IPublicModelNode, at?: number | null): void { + return this[nodeChildrenSymbol].insert((node as any)?.[nodeSymbol], at); + } + + /** + * 返回指定节点的下标 + * @param node + * @returns + */ + indexOf(node: IPublicModelNode): number { + return this[nodeChildrenSymbol].indexOf((node as any)?.[nodeSymbol]); + } + + /** + * 类似数组 splice 操作 + * @param start + * @param deleteCount + * @param node + */ + splice(start: number, deleteCount: number, node?: IPublicModelNode): any { + this[nodeChildrenSymbol].splice(start, deleteCount, (node as any)?.[nodeSymbol]); + } + + /** + * 返回指定下标的节点 + * @param index + * @returns + */ + get(index: number): IPublicModelNode | null { + return ShellNode.create(this[nodeChildrenSymbol].get(index)); + } + + /** + * 是否包含指定节点 + * @param node + * @returns + */ + has(node: IPublicModelNode): boolean { + return this[nodeChildrenSymbol].has((node as any)?.[nodeSymbol]); + } + + /** + * 类似数组的 forEach + * @param fn + */ + forEach(fn: (node: IPublicModelNode, index: number) => void): void { + this[nodeChildrenSymbol].forEach((item: InnerNode, index: number) => { + fn(ShellNode.create(item)!, index); + }); + } + + /** + * 类似数组的 reverse + */ + reverse(): IPublicModelNode[] { + return this[nodeChildrenSymbol].reverse().map(d => { + return ShellNode.create(d)!; + }); + } + + /** + * 类似数组的 map + * @param fn + */ + map<T = any>(fn: (node: IPublicModelNode, index: number) => T): T[] | null { + return this[nodeChildrenSymbol].map<T>((item: InnerNode, index: number): T => { + return fn(ShellNode.create(item)!, index); + }); + } + + /** + * 类似数组的 every + * @param fn + */ + every(fn: (node: IPublicModelNode, index: number) => boolean): boolean { + return this[nodeChildrenSymbol].every((item: InnerNode, index: number) => { + return fn(ShellNode.create(item)!, index); + }); + } + + /** + * 类似数组的 some + * @param fn + */ + some(fn: (node: IPublicModelNode, index: number) => boolean): boolean { + return this[nodeChildrenSymbol].some((item: InnerNode, index: number) => { + return fn(ShellNode.create(item)!, index); + }); + } + + /** + * 类似数组的 filter + * @param fn + */ + filter(fn: (node: IPublicModelNode, index: number) => boolean): any { + return this[nodeChildrenSymbol] + .filter((item: InnerNode, index: number) => { + return fn(ShellNode.create(item)!, index); + }) + .map((item: InnerNode) => ShellNode.create(item)!); + } + + /** + * 类似数组的 find + * @param fn + */ + find(fn: (node: IPublicModelNode, index: number) => boolean): IPublicModelNode | null { + return ShellNode.create( + this[nodeChildrenSymbol].find((item: InnerNode, index: number) => { + return fn(ShellNode.create(item)!, index); + }), + ); + } + + /** + * 类似数组的 reduce + * @param fn + */ + reduce(fn: (acc: any, cur: IPublicModelNode) => any, initialValue: any): void { + return this[nodeChildrenSymbol].reduce((acc: any, cur: InnerNode) => { + return fn(acc, ShellNode.create(cur)!); + }, initialValue); + } + + /** + * 导入 schema + * @param data + */ + importSchema(data?: IPublicTypeNodeData | IPublicTypeNodeData[]): void { + this[nodeChildrenSymbol].import(data); + } + + /** + * 导出 schema + * @param stage + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render): any { + return this[nodeChildrenSymbol].export(stage); + } + + /** + * 执行新增、删除、排序等操作 + * @param remover + * @param adder + * @param sorter + */ + mergeChildren( + remover: (node: IPublicModelNode, idx: number) => boolean, + adder: (children: IPublicModelNode[]) => any, + originalSorter: (firstNode: IPublicModelNode, secondNode: IPublicModelNode) => number, + ) { + let sorter = originalSorter; + if (!sorter) { + sorter = () => 0; + } + this[nodeChildrenSymbol].mergeChildren( + (node: InnerNode, idx: number) => remover(ShellNode.create(node)!, idx), + (children: InnerNode[]) => adder(children.map((node) => ShellNode.create(node)!)), + (firstNode: InnerNode, secondNode: InnerNode) => { + return sorter(ShellNode.create(firstNode)!, ShellNode.create(secondNode)!); + }, + ); + } +} diff --git a/packages/shell/src/model/node.ts b/packages/shell/src/model/node.ts new file mode 100644 index 0000000000..29d24232eb --- /dev/null +++ b/packages/shell/src/model/node.ts @@ -0,0 +1,678 @@ +import { + IDocumentModel as InnerDocumentModel, + INode as InnerNode, +} from '@alilc/lowcode-designer'; +import { + IPublicTypeCompositeValue, + IPublicTypeNodeSchema, + IPublicEnumTransformStage, + IPublicModelNode, + IPublicTypeIconType, + IPublicTypeI18nData, + IPublicModelComponentMeta, + IPublicModelDocumentModel, + IPublicModelNodeChildren, + IPublicModelProp, + IPublicModelProps, + IPublicTypePropsMap, + IPublicTypePropsList, + IPublicModelSettingTopEntry, + IPublicModelExclusiveGroup, +} from '@alilc/lowcode-types'; +import { Prop as ShellProp } from './prop'; +import { Props as ShellProps } from './props'; +import { DocumentModel as ShellDocumentModel } from './document-model'; +import { NodeChildren as ShellNodeChildren } from './node-children'; +import { ComponentMeta as ShellComponentMeta } from './component-meta'; +import { SettingTopEntry as ShellSettingTopEntry } from './setting-top-entry'; +import { documentSymbol, nodeSymbol } from '../symbols'; +import { ReactElement } from 'react'; +import { ConditionGroup } from './condition-group'; + +const shellNodeSymbol = Symbol('shellNodeSymbol'); + +function isShellNode(node: any): node is IPublicModelNode { + return node[shellNodeSymbol]; +} + +export class Node implements IPublicModelNode { + private readonly [documentSymbol]: InnerDocumentModel | null; + private readonly [nodeSymbol]: InnerNode; + + private _id: string; + + /** + * 节点 id + */ + get id() { + return this._id; + } + + /** + * set id + */ + set id(id: string) { + this._id = id; + } + + /** + * 节点标题 + */ + get title(): string | IPublicTypeI18nData | ReactElement { + return this[nodeSymbol].title; + } + + /** + * @deprecated + * 是否为「容器型」节点 + */ + get isContainer(): boolean { + return this[nodeSymbol].isContainerNode; + } + + /** + * 是否为「容器型」节点 + */ + get isContainerNode(): boolean { + return this[nodeSymbol].isContainerNode; + } + + /** + * @deprecated + * 是否为根节点 + */ + get isRoot(): boolean { + return this[nodeSymbol].isRootNode; + } + + /** + * 是否为根节点 + */ + get isRootNode(): boolean { + return this[nodeSymbol].isRootNode; + } + + /** + * @deprecated + * 是否为空节点(无 children 或者 children 为空) + */ + get isEmpty(): boolean { + return this[nodeSymbol].isEmptyNode; + } + + /** + * 是否为空节点(无 children 或者 children 为空) + */ + get isEmptyNode(): boolean { + return this[nodeSymbol].isEmptyNode; + } + + /** + * @deprecated + * 是否为 Page 节点 + */ + get isPage(): boolean { + return this[nodeSymbol].isPageNode; + } + + /** + * 是否为 Page 节点 + */ + get isPageNode(): boolean { + return this[nodeSymbol].isPageNode; + } + + /** + * @deprecated + * 是否为 Component 节点 + */ + get isComponent(): boolean { + return this[nodeSymbol].isComponentNode; + } + + /** + * 是否为 Component 节点 + */ + get isComponentNode(): boolean { + return this[nodeSymbol].isComponentNode; + } + + /** + * @deprecated + * 是否为「模态框」节点 + */ + get isModal(): boolean { + return this[nodeSymbol].isModalNode; + } + + /** + * 是否为「模态框」节点 + */ + get isModalNode(): boolean { + return this[nodeSymbol].isModalNode; + } + + /** + * @deprecated + * 是否为插槽节点 + */ + get isSlot(): boolean { + return this[nodeSymbol].isSlotNode; + } + + /** + * 是否为插槽节点 + */ + get isSlotNode(): boolean { + return this[nodeSymbol].isSlotNode; + } + + /** + * @deprecated + * 是否为父类/分支节点 + */ + get isParental(): boolean { + return this[nodeSymbol].isParentalNode; + } + + /** + * 是否为父类/分支节点 + */ + get isParentalNode(): boolean { + return this[nodeSymbol].isParentalNode; + } + + /** + * @deprecated + * 是否为叶子节点 + */ + get isLeaf(): boolean { + return this[nodeSymbol].isLeafNode; + } + + /** + * 是否为叶子节点 + */ + get isLeafNode(): boolean { + return this[nodeSymbol].isLeafNode; + } + + /** + * judge if it is a node or not + */ + readonly isNode = true; + + /** + * 获取当前节点的锁定状态 + */ + get isLocked(): boolean { + return this[nodeSymbol].isLocked; + } + + /** + * 下标 + */ + get index() { + return this[nodeSymbol].index; + } + + /** + * 图标 + */ + get icon(): IPublicTypeIconType { + return this[nodeSymbol].icon; + } + + /** + * 节点所在树的层级深度,根节点深度为 0 + */ + get zLevel(): number { + return this[nodeSymbol].zLevel; + } + + /** + * 节点 componentName + */ + get componentName(): string { + return this[nodeSymbol].componentName; + } + + /** + * 节点的物料元数据 + */ + get componentMeta(): IPublicModelComponentMeta | null { + return ShellComponentMeta.create(this[nodeSymbol].componentMeta); + } + + /** + * 获取节点所属的文档模型对象 + * @returns + */ + get document(): IPublicModelDocumentModel | null { + return ShellDocumentModel.create(this[documentSymbol]); + } + + /** + * 获取当前节点的前一个兄弟节点 + * @returns + */ + get prevSibling(): IPublicModelNode | null { + return Node.create(this[nodeSymbol].prevSibling); + } + + /** + * 获取当前节点的后一个兄弟节点 + * @returns + */ + get nextSibling(): IPublicModelNode | null { + return Node.create(this[nodeSymbol].nextSibling); + } + + /** + * 获取当前节点的父亲节点 + * @returns + */ + get parent(): IPublicModelNode | null { + return Node.create(this[nodeSymbol].parent); + } + + /** + * 获取当前节点的孩子节点模型 + * @returns + */ + get children(): IPublicModelNodeChildren | null { + return ShellNodeChildren.create(this[nodeSymbol].children); + } + + /** + * 节点上挂载的插槽节点们 + */ + get slots(): IPublicModelNode[] { + return this[nodeSymbol].slots.map((node: InnerNode) => Node.create(node)!); + } + + /** + * 当前节点为插槽节点时,返回节点对应的属性实例 + */ + get slotFor(): IPublicModelProp | null | undefined { + return ShellProp.create(this[nodeSymbol].slotFor); + } + + /** + * 返回节点的属性集 + */ + get props(): IPublicModelProps | null { + return ShellProps.create(this[nodeSymbol].props); + } + + /** + * 返回节点的属性集 + */ + get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null { + return this[nodeSymbol].propsData; + } + + /** + * 获取符合搭建协议 - 节点 schema 结构 + */ + get schema(): IPublicTypeNodeSchema { + return this[nodeSymbol].schema; + } + + get settingEntry(): IPublicModelSettingTopEntry { + return ShellSettingTopEntry.create(this[nodeSymbol].settingEntry as any); + } + + constructor(node: InnerNode) { + this[nodeSymbol] = node; + this[documentSymbol] = node.document; + + this._id = this[nodeSymbol].id; + } + + static create(node: InnerNode | IPublicModelNode | null | undefined): IPublicModelNode | null { + if (!node) { + return null; + } + // @ts-ignore 直接返回已挂载的 shell node 实例 + if (isShellNode(node)) { + return (node as any)[shellNodeSymbol]; + } + const shellNode = new Node(node); + // @ts-ignore 挂载 shell node 实例 + // eslint-disable-next-line no-param-reassign + node[shellNodeSymbol] = shellNode; + return shellNode; + } + + /** + * @deprecated use .children instead + */ + getChildren() { + return this.children; + } + + /** + * 获取节点实例对应的 dom 节点 + */ + getDOMNode() { + return (this[nodeSymbol] as any).getDOMNode(); + } + + /** + * 执行新增、删除、排序等操作 + * @param remover + * @param adder + * @param sorter + */ + mergeChildren( + remover: (node: IPublicModelNode, idx: number) => boolean, + adder: (children: IPublicModelNode[]) => any, + sorter: (firstNode: IPublicModelNode, secondNode: IPublicModelNode) => number, + ): any { + return this.children?.mergeChildren(remover, adder, sorter); + } + + /** + * 返回节点的尺寸、位置信息 + * @returns + */ + getRect(): DOMRect | null { + return this[nodeSymbol].getRect(); + } + + /** + * 是否有挂载插槽节点 + * @returns + */ + hasSlots(): boolean { + return this[nodeSymbol].hasSlots(); + } + + /** + * 是否设定了渲染条件 + * @returns + */ + hasCondition(): boolean { + return this[nodeSymbol].hasCondition(); + } + + /** + * 是否设定了循环数据 + * @returns + */ + hasLoop(): boolean { + return this[nodeSymbol].hasLoop(); + } + + get visible(): boolean { + return this[nodeSymbol].getVisible(); + } + + set visible(value: boolean) { + this[nodeSymbol].setVisible(value); + } + + getVisible(): boolean { + return this[nodeSymbol].getVisible(); + } + + setVisible(flag: boolean): void { + this[nodeSymbol].setVisible(flag); + } + + isConditionalVisible(): boolean | undefined { + return this[nodeSymbol].isConditionalVisible(); + } + + /** + * 设置节点锁定状态 + * @param flag + */ + lock(flag?: boolean): void { + this[nodeSymbol].lock(flag); + } + + /** + * @deprecated use .props instead + */ + getProps() { + return this.props; + } + + contains(node: IPublicModelNode): boolean { + return this[nodeSymbol].contains((node as any)[nodeSymbol]); + } + + /** + * 获取指定 path 的属性模型实例 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getProp(path: string, createIfNone = true): IPublicModelProp | null { + return ShellProp.create(this[nodeSymbol].getProp(path, createIfNone)); + } + + /** + * 获取指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getPropValue(path: string) { + return this.getProp(path, false)?.getValue(); + } + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param createIfNone 当没有属性的时候,是否创建一个属性 + * @returns + */ + getExtraProp(path: string, createIfNone?: boolean): IPublicModelProp | null { + return ShellProp.create(this[nodeSymbol].getExtraProp(path, createIfNone)); + } + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getExtraPropValue(path: string): any { + return this.getExtraProp(path)?.getValue(); + } + + /** + * 设置指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + * @returns + */ + setPropValue(path: string, value: IPublicTypeCompositeValue): void { + return this.getProp(path)?.setValue(value); + } + + /** + * 设置指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + * @returns + */ + setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void { + return this.getExtraProp(path)?.setValue(value); + } + + /** + * 导入节点数据 + * @param data + */ + importSchema(data: IPublicTypeNodeSchema): void { + this[nodeSymbol].import(data); + } + + /** + * 导出节点数据 + * @param stage + * @param options + * @returns + */ + exportSchema( + stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render, + options?: any, + ): IPublicTypeNodeSchema { + return this[nodeSymbol].export(stage, options); + } + + /** + * 在指定位置之前插入一个节点 + * @param node + * @param ref + * @param useMutator + */ + insertBefore( + node: IPublicModelNode, + ref?: IPublicModelNode | undefined, + useMutator?: boolean, + ): void { + this[nodeSymbol].insertBefore( + (node as any)[nodeSymbol] || node, + (ref as any)?.[nodeSymbol], + useMutator, + ); + } + + /** + * 在指定位置之后插入一个节点 + * @param node + * @param ref + * @param useMutator + */ + insertAfter( + node: IPublicModelNode, + ref?: IPublicModelNode | undefined, + useMutator?: boolean, + ): void { + this[nodeSymbol].insertAfter( + (node as any)[nodeSymbol] || node, + (ref as any)?.[nodeSymbol], + useMutator, + ); + } + + /** + * 替换指定节点 + * @param node 待替换的子节点 + * @param data 用作替换的节点对象或者节点描述 + * @returns + */ + replaceChild(node: IPublicModelNode, data: any): IPublicModelNode | null { + return Node.create(this[nodeSymbol].replaceChild((node as any)[nodeSymbol], data)); + } + + /** + * 将当前节点替换成指定节点描述 + * @param schema + */ + replaceWith(schema: IPublicTypeNodeSchema): any { + this[nodeSymbol].replaceWith(schema); + } + + /** + * 选中当前节点实例 + */ + select(): void { + this[nodeSymbol].select(); + } + + /** + * 设置悬停态 + * @param flag + */ + hover(flag = true): void { + this[nodeSymbol].hover(flag); + } + + /** + * 删除当前节点实例 + */ + remove(): void { + this[nodeSymbol].remove(); + } + + /** + * @deprecated + * 设置为磁贴布局节点 + */ + set isRGLContainer(flag: boolean) { + this[nodeSymbol].isRGLContainerNode = flag; + } + + /** + * @deprecated + * 获取磁贴布局节点设置状态 + * @returns Boolean + */ + get isRGLContainer() { + return this[nodeSymbol].isRGLContainerNode; + } + + /** + * 设置为磁贴布局节点 + */ + set isRGLContainerNode(flag: boolean) { + this[nodeSymbol].isRGLContainerNode = flag; + } + + /** + * 获取磁贴布局节点设置状态 + * @returns Boolean + */ + get isRGLContainerNode() { + return this[nodeSymbol].isRGLContainerNode; + } + + internalToShellNode() { + return this; + } + + canPerformAction(actionName: string): boolean { + return this[nodeSymbol].canPerformAction(actionName); + } + + /** + * get conditionGroup + * @since v1.1.0 + */ + get conditionGroup(): IPublicModelExclusiveGroup | null { + return ConditionGroup.create(this[nodeSymbol].conditionGroup); + } + + /** + * set value for conditionalVisible + * @since v1.1.0 + */ + setConditionalVisible(): void { + this[nodeSymbol].setConditionalVisible(); + } + + getRGL() { + const { + isContainerNode, + isEmptyNode, + isRGLContainerNode, + isRGLNode, + isRGL, + rglNode, + } = this[nodeSymbol].getRGL(); + + return { + isContainerNode, + isEmptyNode, + isRGLContainerNode, + isRGLNode, + isRGL, + rglNode: Node.create(rglNode), + }; + } +} diff --git a/packages/shell/src/model/plugin-instance.ts b/packages/shell/src/model/plugin-instance.ts new file mode 100644 index 0000000000..156ec7579c --- /dev/null +++ b/packages/shell/src/model/plugin-instance.ts @@ -0,0 +1,31 @@ +import { ILowCodePluginRuntime } from '@alilc/lowcode-designer'; +import { IPublicModelPluginInstance } from '@alilc/lowcode-types'; +import { pluginInstanceSymbol } from '../symbols'; + +export class PluginInstance implements IPublicModelPluginInstance { + private readonly [pluginInstanceSymbol]: ILowCodePluginRuntime; + + constructor(pluginInstance: ILowCodePluginRuntime) { + this[pluginInstanceSymbol] = pluginInstance; + } + + get pluginName(): string { + return this[pluginInstanceSymbol].name; + } + + get dep(): string[] { + return this[pluginInstanceSymbol].dep; + } + + get disabled(): boolean { + return this[pluginInstanceSymbol].disabled; + } + + set disabled(disabled: boolean) { + this[pluginInstanceSymbol].setDisabled(disabled); + } + + get meta() { + return this[pluginInstanceSymbol].meta; + } +} diff --git a/packages/shell/src/model/prop.ts b/packages/shell/src/model/prop.ts new file mode 100644 index 0000000000..8d4ca7842e --- /dev/null +++ b/packages/shell/src/model/prop.ts @@ -0,0 +1,94 @@ +import { IProp as InnerProp } from '@alilc/lowcode-designer'; +import { IPublicTypeCompositeValue, IPublicEnumTransformStage, IPublicModelProp, IPublicModelNode } from '@alilc/lowcode-types'; +import { propSymbol } from '../symbols'; +import { Node as ShellNode } from './node'; + +export class Prop implements IPublicModelProp { + private readonly [propSymbol]: InnerProp; + + constructor(prop: InnerProp) { + this[propSymbol] = prop; + } + + static create(prop: InnerProp | undefined | null): IPublicModelProp | null { + if (!prop) { + return null; + } + return new Prop(prop); + } + + /** + * id + */ + get id(): string { + return this[propSymbol].id; + } + + /** + * key 值 + * get key of prop + */ + get key(): string | number | undefined { + return this[propSymbol].key; + } + + /** + * 返回当前 prop 的路径 + */ + get path(): string[] { + return this[propSymbol].path; + } + + /** + * 返回所属的节点实例 + */ + get node(): IPublicModelNode | null { + return ShellNode.create(this[propSymbol].getNode()); + } + + /** + * return the slot node (only if the current prop represents a slot) + */ + get slotNode(): IPublicModelNode | null { + return ShellNode.create(this[propSymbol].slotNode); + } + + /** + * judge if it is a prop or not + */ + get isProp(): boolean { + return true; + } + + /** + * 设置值 + * @param val + */ + setValue(val: IPublicTypeCompositeValue): void { + this[propSymbol].setValue(val); + } + + /** + * 获取值 + * @returns + */ + getValue(): any { + return this[propSymbol].getValue(); + } + + /** + * 移除值 + */ + remove(): void { + this[propSymbol].remove(); + } + + /** + * 导出值 + * @param stage + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage = IPublicEnumTransformStage.Render) { + return this[propSymbol].export(stage); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/props.ts b/packages/shell/src/model/props.ts new file mode 100644 index 0000000000..86a9a2142b --- /dev/null +++ b/packages/shell/src/model/props.ts @@ -0,0 +1,118 @@ +import { IProps as InnerProps, getConvertedExtraKey } from '@alilc/lowcode-designer'; +import { IPublicTypeCompositeValue, IPublicModelProps, IPublicModelNode, IPublicModelProp } from '@alilc/lowcode-types'; +import { propsSymbol } from '../symbols'; +import { Node as ShellNode } from './node'; +import { Prop as ShellProp } from './prop'; + +export class Props implements IPublicModelProps { + private readonly [propsSymbol]: InnerProps; + + constructor(props: InnerProps) { + this[propsSymbol] = props; + } + + static create(props: InnerProps | undefined | null): IPublicModelProps | null { + if (!props) { + return null; + } + return new Props(props); + } + + /** + * id + */ + get id(): string { + return this[propsSymbol].id; + } + + /** + * 返回当前 props 的路径 + */ + get path(): string[] { + return this[propsSymbol].path; + } + + /** + * 返回所属的 node 实例 + */ + get node(): IPublicModelNode | null { + return ShellNode.create(this[propsSymbol].getNode()); + } + + /** + * 获取指定 path 的属性模型实例 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getProp(path: string): IPublicModelProp | null { + return ShellProp.create(this[propsSymbol].getProp(path)); + } + + /** + * 获取指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getPropValue(path: string): any { + return this.getProp(path)?.getValue(); + } + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getExtraProp(path: string): IPublicModelProp | null { + return ShellProp.create(this[propsSymbol].getProp(getConvertedExtraKey(path))); + } + + /** + * 获取指定 path 的属性模型实例值 + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getExtraPropValue(path: string): any { + return this.getExtraProp(path)?.getValue(); + } + + /** + * 设置指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + * @returns + */ + setPropValue(path: string, value: IPublicTypeCompositeValue): void { + return this.getProp(path)?.setValue(value); + } + + /** + * 设置指定 path 的属性模型实例值 + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + * @returns + */ + setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void { + return this.getExtraProp(path)?.setValue(value); + } + + /** + * test if the specified key is existing or not. + * @param key + * @returns + */ + has(key: string): boolean { + return this[propsSymbol].has(key); + } + + /** + * add a key with given value + * @param value + * @param key + * @returns + */ + add(value: IPublicTypeCompositeValue, key?: string | number | undefined): any { + return this[propsSymbol].add(value, key); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/resource.ts b/packages/shell/src/model/resource.ts new file mode 100644 index 0000000000..29a385b993 --- /dev/null +++ b/packages/shell/src/model/resource.ts @@ -0,0 +1,55 @@ +import { IPublicModelResource } from '@alilc/lowcode-types'; +import { IResource } from '@alilc/lowcode-workspace'; +import { resourceSymbol } from '../symbols'; + +export class Resource implements IPublicModelResource { + readonly [resourceSymbol]: IResource; + + constructor(resource: IResource) { + this[resourceSymbol] = resource; + } + + get title() { + return this[resourceSymbol].title; + } + + get id() { + return this[resourceSymbol].id; + } + + get icon() { + return this[resourceSymbol].icon; + } + + get options() { + return this[resourceSymbol].options; + } + + get name() { + return this[resourceSymbol].resourceType.name; + } + + get config() { + return this[resourceSymbol].config; + } + + get type() { + return this[resourceSymbol].resourceType.type; + } + + get category() { + return this[resourceSymbol].category; + } + + get description() { + return this[resourceSymbol].description; + } + + get children() { + return this[resourceSymbol].children.map((child) => new Resource(child)); + } + + get viewName() { + return this[resourceSymbol].viewName; + } +} \ No newline at end of file diff --git a/packages/shell/src/model/selection.ts b/packages/shell/src/model/selection.ts new file mode 100644 index 0000000000..073083a650 --- /dev/null +++ b/packages/shell/src/model/selection.ts @@ -0,0 +1,118 @@ +import { + IDocumentModel as InnerDocumentModel, + INode as InnerNode, + ISelection, +} from '@alilc/lowcode-designer'; +import { Node as ShellNode } from './node'; +import { selectionSymbol } from '../symbols'; +import { IPublicModelSelection, IPublicModelNode, IPublicTypeDisposable } from '@alilc/lowcode-types'; + +export class Selection implements IPublicModelSelection { + private readonly [selectionSymbol]: ISelection; + + constructor(document: InnerDocumentModel) { + this[selectionSymbol] = document.selection; + } + + /** + * 返回选中的节点 id + */ + get selected(): string[] { + return this[selectionSymbol].selected; + } + + /** + * return selected Node instance + */ + get node(): IPublicModelNode | null { + const nodes = this.getNodes(); + return nodes && nodes.length > 0 ? nodes[0] : null; + } + + /** + * 选中指定节点(覆盖方式) + * @param id + */ + select(id: string): void { + this[selectionSymbol].select(id); + } + + /** + * 批量选中指定节点们 + * @param ids + */ + selectAll(ids: string[]): void { + this[selectionSymbol].selectAll(ids); + } + + /** + * 移除选中的指定节点 + * @param id + */ + remove(id: string): void { + this[selectionSymbol].remove(id); + } + + /** + * 清除所有选中节点 + */ + clear(): void { + this[selectionSymbol].clear(); + } + + /** + * 判断是否选中了指定节点 + * @param id + * @returns + */ + has(id: string): boolean { + return this[selectionSymbol].has(id); + } + + /** + * 选中指定节点(增量方式) + * @param id + */ + add(id: string): void { + this[selectionSymbol].add(id); + } + + /** + * 获取选中的节点实例 + * @returns + */ + getNodes(): IPublicModelNode[] { + const innerNodes = this[selectionSymbol].getNodes(); + const nodes: IPublicModelNode[] = []; + innerNodes.forEach((node: InnerNode) => { + const shellNode = ShellNode.create(node); + if (shellNode) { + nodes.push(shellNode); + } + }); + return nodes; + } + + /** + * 获取选区的顶层节点 + * for example: + * getNodes() returns [A, subA, B], then + * getTopNodes() will return [A, B], subA will be removed + * @returns + */ + getTopNodes(includeRoot: boolean = false): IPublicModelNode[] { + const innerNodes = this[selectionSymbol].getTopNodes(includeRoot); + const nodes: IPublicModelNode[] = []; + innerNodes.forEach((node: InnerNode) => { + const shellNode = ShellNode.create(node); + if (shellNode) { + nodes.push(shellNode); + } + }); + return nodes; + } + + onSelectionChange(fn: (ids: string[]) => void): IPublicTypeDisposable { + return this[selectionSymbol].onSelectionChange(fn); + } +} diff --git a/packages/shell/src/model/setting-field.ts b/packages/shell/src/model/setting-field.ts new file mode 100644 index 0000000000..ffc97ccc8f --- /dev/null +++ b/packages/shell/src/model/setting-field.ts @@ -0,0 +1,307 @@ +import { ISettingField, isSettingField } from '@alilc/lowcode-designer'; +import { + IPublicTypeCompositeValue, + IPublicTypeFieldConfig, + IPublicTypeCustomView, + IPublicTypeSetterType, + IPublicTypeFieldExtraProps, + IPublicModelSettingTopEntry, + IPublicModelNode, + IPublicModelComponentMeta, + IPublicTypeSetValueOptions, + IPublicModelSettingField, + IPublicTypeDisposable, +} from '@alilc/lowcode-types'; +import { settingFieldSymbol } from '../symbols'; +import { Node as ShellNode } from './node'; +import { SettingTopEntry, SettingTopEntry as ShellSettingTopEntry } from './setting-top-entry'; +import { ComponentMeta as ShellComponentMeta } from './component-meta'; +import { isCustomView } from '@alilc/lowcode-utils'; + +export class SettingField implements IPublicModelSettingField { + private readonly [settingFieldSymbol]: ISettingField; + + constructor(prop: ISettingField) { + this[settingFieldSymbol] = prop; + } + + static create(prop: ISettingField): IPublicModelSettingField { + return new SettingField(prop); + } + + /** + * 获取设置属性的 isGroup + */ + get isGroup(): boolean { + return this[settingFieldSymbol].isGroup; + } + + /** + * 获取设置属性的 id + */ + get id(): string { + return this[settingFieldSymbol].id; + } + + /** + * 获取设置属性的 name + */ + get name(): string | number | undefined { + return this[settingFieldSymbol].name; + } + + /** + * 获取设置属性的 key + */ + get key(): string | number | undefined { + return this[settingFieldSymbol].getKey(); + } + + /** + * 获取设置属性的 path + */ + get path(): any[] { + return this[settingFieldSymbol].path; + } + + /** + * 获取设置属性的 title + */ + get title(): any { + return this[settingFieldSymbol].title; + } + + /** + * 获取设置属性的 setter + */ + get setter(): IPublicTypeSetterType | null { + return this[settingFieldSymbol].setter; + } + + /** + * 获取设置属性的 expanded + */ + get expanded(): boolean { + return this[settingFieldSymbol].expanded; + } + + /** + * 获取设置属性的 extraProps + */ + get extraProps(): IPublicTypeFieldExtraProps { + return this[settingFieldSymbol].extraProps; + } + + get props(): IPublicModelSettingTopEntry { + return ShellSettingTopEntry.create(this[settingFieldSymbol].props); + } + + /** + * 获取设置属性对应的节点实例 + */ + get node(): IPublicModelNode | null { + return ShellNode.create(this[settingFieldSymbol].getNode()); + } + + /** + * 获取设置属性的父设置属性 + */ + get parent(): IPublicModelSettingField | IPublicModelSettingTopEntry { + if (isSettingField(this[settingFieldSymbol].parent)) { + return SettingField.create(this[settingFieldSymbol].parent); + } + + return SettingTopEntry.create(this[settingFieldSymbol].parent); + } + + /** + * 获取顶级设置属性 + */ + get top(): IPublicModelSettingTopEntry { + return ShellSettingTopEntry.create(this[settingFieldSymbol].top); + } + + /** + * 是否是 SettingField 实例 + */ + get isSettingField(): boolean { + return this[settingFieldSymbol].isSettingField; + } + + /** + * componentMeta + */ + get componentMeta(): IPublicModelComponentMeta | null { + return ShellComponentMeta.create(this[settingFieldSymbol].componentMeta); + } + + /** + * 获取设置属性的 items + */ + get items(): Array<IPublicModelSettingField | IPublicTypeCustomView> { + return this[settingFieldSymbol].items?.map((item) => { + if (isCustomView(item)) { + return item; + } + return item.internalToShellField(); + }); + } + + /** + * 设置 key 值 + * @param key + */ + setKey(key: string | number): void { + this[settingFieldSymbol].setKey(key); + } + + /** + * @deprecated use .node instead + */ + getNode() { + return this.node; + } + + /** + * @deprecated use .parent instead + */ + getParent() { + return this.parent; + } + + /** + * 设置值 + * @param val 值 + */ + setValue(val: IPublicTypeCompositeValue, extraOptions?: IPublicTypeSetValueOptions): void { + this[settingFieldSymbol].setValue(val, false, false, extraOptions); + } + + /** + * 设置子级属性值 + * @param propName 子属性名 + * @param value 值 + */ + setPropValue(propName: string | number, value: any): void { + this[settingFieldSymbol].setPropValue(propName, value); + } + + /** + * 清空指定属性值 + * @param propName + */ + clearPropValue(propName: string | number): void { + this[settingFieldSymbol].clearPropValue(propName); + } + + /** + * 获取配置的默认值 + * @returns + */ + getDefaultValue(): any { + return this[settingFieldSymbol].getDefaultValue(); + } + + /** + * 获取值 + * @returns + */ + getValue(): any { + return this[settingFieldSymbol].getValue(); + } + + /** + * 获取子级属性值 + * @param propName 子属性名 + * @returns + */ + getPropValue(propName: string | number): any { + return this[settingFieldSymbol].getPropValue(propName); + } + + /** + * 获取顶层附属属性值 + */ + getExtraPropValue(propName: string): any { + return this[settingFieldSymbol].getExtraPropValue(propName); + } + + /** + * 设置顶层附属属性值 + */ + setExtraPropValue(propName: string, value: any): void { + this[settingFieldSymbol].setExtraPropValue(propName, value); + } + + /** + * 获取设置属性集 + * @returns + */ + getProps(): IPublicModelSettingTopEntry { + return ShellSettingTopEntry.create(this[settingFieldSymbol].getProps()); + } + + /** + * 是否绑定了变量 + * @returns + */ + isUseVariable(): boolean { + return this[settingFieldSymbol].isUseVariable(); + } + + /** + * 设置绑定变量 + * @param flag + */ + setUseVariable(flag: boolean): void { + this[settingFieldSymbol].setUseVariable(flag); + } + + /** + * 创建一个设置 field 实例 + * @param config + * @returns + */ + createField(config: IPublicTypeFieldConfig): IPublicModelSettingField { + return SettingField.create(this[settingFieldSymbol].createField(config)); + } + + /** + * 获取值,当为变量时,返回 mock + * @returns + */ + getMockOrValue(): any { + return this[settingFieldSymbol].getMockOrValue(); + } + + /** + * 销毁当前 field 实例 + */ + purge(): void { + this[settingFieldSymbol].purge(); + } + + /** + * 移除当前 field 实例 + */ + remove(): void { + this[settingFieldSymbol].remove(); + } + + /** + * 设置 autorun + * @param action + * @returns + */ + onEffect(action: () => void): IPublicTypeDisposable { + return this[settingFieldSymbol].onEffect(action); + } + + /** + * 返回 shell 模型,兼容某些场景下 field 已经是 shell field 了 + * @returns + */ + internalToShellField() { + return this; + } +} diff --git a/packages/shell/src/model/setting-top-entry.ts b/packages/shell/src/model/setting-top-entry.ts new file mode 100644 index 0000000000..8afed43a50 --- /dev/null +++ b/packages/shell/src/model/setting-top-entry.ts @@ -0,0 +1,62 @@ +import { ISettingTopEntry } from '@alilc/lowcode-designer'; +import { settingTopEntrySymbol } from '../symbols'; +import { Node as ShellNode } from './node'; +import { IPublicModelSettingTopEntry, IPublicModelNode, IPublicModelSettingField } from '@alilc/lowcode-types'; +import { SettingField } from './setting-field'; + +export class SettingTopEntry implements IPublicModelSettingTopEntry { + private readonly [settingTopEntrySymbol]: ISettingTopEntry; + + constructor(prop: ISettingTopEntry) { + this[settingTopEntrySymbol] = prop; + } + + static create(prop: ISettingTopEntry): IPublicModelSettingTopEntry { + return new SettingTopEntry(prop); + } + + /** + * 返回所属的节点实例 + */ + get node(): IPublicModelNode | null { + return ShellNode.create(this[settingTopEntrySymbol].getNode()); + } + + /** + * 获取子级属性对象 + * @param propName + * @returns + */ + get(propName: string | number): IPublicModelSettingField { + return SettingField.create(this[settingTopEntrySymbol].get(propName)!); + } + + /** + * @deprecated use .node instead + */ + getNode() { + return this.node; + } + + /** + * 获取指定 propName 的值 + * @param propName + * @returns + */ + getPropValue(propName: string | number): any { + return this[settingTopEntrySymbol].getPropValue(propName); + } + + /** + * 设置指定 propName 的值 + * @param propName + * @param value + */ + setPropValue(propName: string | number, value: any): void { + this[settingTopEntrySymbol].setPropValue(propName, value); + } + + clearPropValue(propName: string | number) { + this[settingTopEntrySymbol].clearPropValue(propName); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/simulator-render.ts b/packages/shell/src/model/simulator-render.ts new file mode 100644 index 0000000000..f6ae47996c --- /dev/null +++ b/packages/shell/src/model/simulator-render.ts @@ -0,0 +1,23 @@ +import { IPublicModelSimulatorRender } from '@alilc/lowcode-types'; +import { simulatorRenderSymbol } from '../symbols'; +import { BuiltinSimulatorRenderer } from '@alilc/lowcode-designer'; + +export class SimulatorRender implements IPublicModelSimulatorRender { + private readonly [simulatorRenderSymbol]: BuiltinSimulatorRenderer; + + constructor(simulatorRender: BuiltinSimulatorRenderer) { + this[simulatorRenderSymbol] = simulatorRender; + } + + static create(simulatorRender: BuiltinSimulatorRenderer): IPublicModelSimulatorRender { + return new SimulatorRender(simulatorRender); + } + + get components() { + return this[simulatorRenderSymbol].components; + } + + rerender() { + return this[simulatorRenderSymbol].rerender(); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/skeleton-item.ts b/packages/shell/src/model/skeleton-item.ts new file mode 100644 index 0000000000..7f1224c0d9 --- /dev/null +++ b/packages/shell/src/model/skeleton-item.ts @@ -0,0 +1,39 @@ +import { skeletonItemSymbol } from '../symbols'; +import { IPublicModelSkeletonItem } from '@alilc/lowcode-types'; +import { Dock, IWidget, Panel, PanelDock, Stage, Widget } from '@alilc/lowcode-editor-skeleton'; + +export class SkeletonItem implements IPublicModelSkeletonItem { + private [skeletonItemSymbol]: IWidget | Widget | Panel | Stage | Dock | PanelDock; + + constructor(skeletonItem: IWidget | Widget | Panel | Stage | Dock | PanelDock) { + this[skeletonItemSymbol] = skeletonItem; + } + + get name() { + return this[skeletonItemSymbol].name; + } + + get visible() { + return this[skeletonItemSymbol].visible; + } + + disable() { + this[skeletonItemSymbol].disable?.(); + } + + enable() { + this[skeletonItemSymbol].enable?.(); + } + + hide() { + this[skeletonItemSymbol].hide(); + } + + show() { + this[skeletonItemSymbol].show(); + } + + toggle() { + this[skeletonItemSymbol].toggle(); + } +} \ No newline at end of file diff --git a/packages/shell/src/model/window.ts b/packages/shell/src/model/window.ts new file mode 100644 index 0000000000..1bc84e661c --- /dev/null +++ b/packages/shell/src/model/window.ts @@ -0,0 +1,60 @@ +import { windowSymbol } from '../symbols'; +import { IPublicModelResource, IPublicModelWindow, IPublicTypeDisposable } from '@alilc/lowcode-types'; +import { IEditorWindow } from '@alilc/lowcode-workspace'; +import { Resource as ShellResource } from './resource'; +import { EditorView } from './editor-view'; + +export class Window implements IPublicModelWindow { + private readonly [windowSymbol]: IEditorWindow; + + get id() { + return this[windowSymbol]?.id; + } + + get title() { + return this[windowSymbol].title; + } + + get icon() { + return this[windowSymbol].icon; + } + + get resource(): IPublicModelResource { + return new ShellResource(this[windowSymbol].resource); + } + + constructor(editorWindow: IEditorWindow) { + this[windowSymbol] = editorWindow; + } + + importSchema(schema: any): any { + this[windowSymbol].importSchema(schema); + } + + changeViewType(viewName: string) { + this[windowSymbol].changeViewName(viewName, false); + } + + onChangeViewType(fun: (viewName: string) => void): IPublicTypeDisposable { + return this[windowSymbol].onChangeViewType(fun); + } + + async save() { + return await this[windowSymbol].save(); + } + + onSave(fn: () => void) { + return this[windowSymbol].onSave(fn); + } + + get currentEditorView() { + if (this[windowSymbol]._editorView) { + return new EditorView(this[windowSymbol]._editorView).toProxy() as any; + } + return null; + } + + get editorViews() { + return Array.from(this[windowSymbol].editorViews.values()).map(d => new EditorView(d).toProxy() as any); + } +} diff --git a/packages/shell/src/node-children.ts b/packages/shell/src/node-children.ts deleted file mode 100644 index 2557e37acb..0000000000 --- a/packages/shell/src/node-children.ts +++ /dev/null @@ -1,215 +0,0 @@ -import { NodeChildren as InnerNodeChildren, Node as InnerNode } from '@alilc/lowcode-designer'; -import { NodeSchema, NodeData, TransformStage } from '@alilc/lowcode-types'; -import Node from './node'; -import { nodeSymbol, nodeChildrenSymbol } from './symbols'; - -export default class NodeChildren { - private readonly [nodeChildrenSymbol]: InnerNodeChildren; - - constructor(nodeChildren: InnerNodeChildren) { - this[nodeChildrenSymbol] = nodeChildren; - } - - static create(nodeChldren: InnerNodeChildren | null) { - if (!nodeChldren) return null; - return new NodeChildren(nodeChldren); - } - - /** - * 返回当前 children 实例所属的节点实例 - */ - get owner(): Node | null { - return Node.create(this[nodeChildrenSymbol].owner); - } - - /** - * children 内的节点实例数 - */ - get size() { - return this[nodeChildrenSymbol].size; - } - - /** - * 是否为空 - * @returns - */ - get isEmpty() { - return this[nodeChildrenSymbol].isEmpty(); - } - - /** - * judge if it is not empty - */ - get notEmpty() { - return !this.isEmpty; - } - - /** - * 删除指定节点 - * @param node - * @returns - */ - delete(node: Node) { - return this[nodeChildrenSymbol].delete(node[nodeSymbol]); - } - - /** - * 插入一个节点 - * @param node 待插入节点 - * @param at 插入下标 - * @returns - */ - insert(node: Node, at?: number | null) { - return this[nodeChildrenSymbol].insert(node[nodeSymbol], at, true); - } - - /** - * 返回指定节点的下标 - * @param node - * @returns - */ - indexOf(node: Node) { - return this[nodeChildrenSymbol].indexOf(node[nodeSymbol]); - } - - /** - * 类似数组 splice 操作 - * @param start - * @param deleteCount - * @param node - */ - splice(start: number, deleteCount: number, node?: Node) { - this[nodeChildrenSymbol].splice(start, deleteCount, node?.[nodeSymbol]); - } - - /** - * 返回指定下标的节点 - * @param index - * @returns - */ - get(index: number) { - return Node.create(this[nodeChildrenSymbol].get(index)); - } - - /** - * 是否包含指定节点 - * @param node - * @returns - */ - has(node: Node) { - return this[nodeChildrenSymbol].has(node[nodeSymbol]); - } - - /** - * 类似数组的 forEach - * @param fn - */ - forEach(fn: (node: Node, index: number) => void) { - this[nodeChildrenSymbol].forEach((item: InnerNode<NodeSchema>, index: number) => { - fn(Node.create(item)!, index); - }); - } - - /** - * 类似数组的 map - * @param fn - */ - map<T>(fn: (node: Node, index: number) => T[]) { - return this[nodeChildrenSymbol].map((item: InnerNode<NodeSchema>, index: number) => { - return fn(Node.create(item)!, index); - }); - } - - /** - * 类似数组的 every - * @param fn - */ - every(fn: (node: Node, index: number) => boolean) { - return this[nodeChildrenSymbol].every((item: InnerNode<NodeSchema>, index: number) => { - return fn(Node.create(item)!, index); - }); - } - - /** - * 类似数组的 some - * @param fn - */ - some(fn: (node: Node, index: number) => boolean) { - return this[nodeChildrenSymbol].some((item: InnerNode<NodeSchema>, index: number) => { - return fn(Node.create(item)!, index); - }); - } - - /** - * 类似数组的 filter - * @param fn - */ - filter(fn: (node: Node, index: number) => boolean) { - return this[nodeChildrenSymbol] - .filter((item: InnerNode<NodeSchema>, index: number) => { - return fn(Node.create(item)!, index); - }) - .map((item: InnerNode<NodeSchema>) => Node.create(item)!); - } - - /** - * 类似数组的 find - * @param fn - */ - find(fn: (node: Node, index: number) => boolean) { - return Node.create( - this[nodeChildrenSymbol].find((item: InnerNode<NodeSchema>, index: number) => { - return fn(Node.create(item)!, index); - }), - ); - } - - /** - * 类似数组的 reduce - * @param fn - */ - reduce(fn: (acc: any, cur: Node) => any, initialValue: any) { - return this[nodeChildrenSymbol].reduce((acc: any, cur: InnerNode) => { - return fn(acc, Node.create(cur)!); - }, initialValue); - } - - /** - * 导入 schema - * @param data - */ - importSchema(data?: NodeData | NodeData[]) { - this[nodeChildrenSymbol].import(data); - } - - /** - * 导出 schema - * @param stage - * @returns - */ - exportSchema(stage: TransformStage = TransformStage.Render) { - return this[nodeChildrenSymbol].export(stage); - } - - /** - * 执行新增、删除、排序等操作 - * @param remover - * @param adder - * @param sorter - */ - mergeChildren( - remover: (node: Node, idx: number) => boolean, - adder: (children: Node[]) => any, - sorter: (firstNode: Node, secondNode: Node) => number, - ) { - if (!sorter) { - sorter = () => 0; - } - this[nodeChildrenSymbol].mergeChildren( - (node: InnerNode, idx: number) => remover(Node.create(node)!, idx), - (children: InnerNode[]) => adder(children.map((node) => Node.create(node)!)), - (firstNode: InnerNode, secondNode: InnerNode) => - sorter(Node.create(firstNode)!, Node.create(secondNode)!), - ); - } -} diff --git a/packages/shell/src/node.ts b/packages/shell/src/node.ts deleted file mode 100644 index ca061dcb5d..0000000000 --- a/packages/shell/src/node.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { - DocumentModel as InnerDocumentModel, - Node as InnerNode, - getConvertedExtraKey, -} from '@alilc/lowcode-designer'; -import { CompositeValue, NodeSchema, TransformStage } from '@alilc/lowcode-types'; -import Prop from './prop'; -import Props from './props'; -import DocumentModel from './document-model'; -import NodeChildren from './node-children'; -import ComponentMeta from './component-meta'; -import SettingTopEntry from './setting-top-entry'; -import { documentSymbol, nodeSymbol } from './symbols'; - -const shellNodeSymbol = Symbol('shellNodeSymbol'); - -export default class Node { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [nodeSymbol]: InnerNode; - - private _id: string; - - constructor(node: InnerNode) { - this[nodeSymbol] = node; - this[documentSymbol] = node.document; - - this._id = this[nodeSymbol].id; - } - - static create(node: InnerNode | null | undefined) { - if (!node) return null; - // @ts-ignore 直接返回已挂载的 shell node 实例 - if (node[shellNodeSymbol]) return node[shellNodeSymbol]; - const shellNode = new Node(node); - // @ts-ignore 挂载 shell node 实例 - node[shellNodeSymbol] = shellNode; - return shellNode; - } - - /** - * 节点 id - */ - get id() { - return this._id; - } - - /** - * set id - */ - set id(id: string) { - this._id = id; - } - - /** - * 节点标题 - */ - get title() { - return this[nodeSymbol].title; - } - - /** - * 是否为「容器型」节点 - */ - get isContainer() { - return this[nodeSymbol].isContainer(); - } - - /** - * 是否为根节点 - */ - get isRoot() { - return this[nodeSymbol].isRoot(); - } - - /** - * 是否为空节点(无 children 或者 children 为空) - */ - get isEmpty() { - return this[nodeSymbol].isEmpty(); - } - - /** - * 是否为 Page 节点 - */ - get isPage() { - return this[nodeSymbol].isPage(); - } - - /** - * 是否为 Component 节点 - */ - get isComponent() { - return this[nodeSymbol].isComponent(); - } - - /** - * 是否为「模态框」节点 - */ - get isModal() { - return this[nodeSymbol].isModal(); - } - - /** - * 是否为插槽节点 - */ - get isSlot() { - return this[nodeSymbol].isSlot(); - } - - /** - * 是否为父类/分支节点 - */ - get isParental() { - return this[nodeSymbol].isParental(); - } - - /** - * 是否为叶子节点 - */ - get isLeaf() { - return this[nodeSymbol].isLeaf(); - } - - /** - * judge if it is a node or not - */ - get isNode() { - return true; - } - - /** - * 下标 - */ - get index() { - return this[nodeSymbol].index; - } - - /** - * 图标 - */ - get icon() { - return this[nodeSymbol].icon; - } - - /** - * 节点所在树的层级深度,根节点深度为 0 - */ - get zLevel() { - return this[nodeSymbol].zLevel; - } - - /** - * 节点 componentName - */ - get componentName() { - return this[nodeSymbol].componentName; - } - - /** - * 节点的物料元数据 - */ - get componentMeta() { - return ComponentMeta.create(this[nodeSymbol].componentMeta); - } - - /** - * 获取节点所属的文档模型对象 - * @returns - */ - get document() { - return DocumentModel.create(this[documentSymbol]); - } - - /** - * 获取当前节点的前一个兄弟节点 - * @returns - */ - get prevSibling(): Node | null { - return Node.create(this[nodeSymbol].prevSibling); - } - - /** - * 获取当前节点的后一个兄弟节点 - * @returns - */ - get nextSibling(): Node | null { - return Node.create(this[nodeSymbol].nextSibling); - } - - /** - * 获取当前节点的父亲节点 - * @returns - */ - get parent(): Node | null { - return Node.create(this[nodeSymbol].parent); - } - - /** - * 获取当前节点的孩子节点模型 - * @returns - */ - get children() { - return NodeChildren.create(this[nodeSymbol].children); - } - - /** - * 节点上挂载的插槽节点们 - */ - get slots(): Node[] { - return this[nodeSymbol].slots.map((node: InnerNode) => Node.create(node)!); - } - - /** - * 当前节点为插槽节点时,返回节点对应的属性实例 - */ - get slotFor() { - return Prop.create(this[nodeSymbol].slotFor); - } - - /** - * 返回节点的属性集 - */ - get props() { - return Props.create(this[nodeSymbol].props); - } - - /** - * 返回节点的属性集 - */ - get propsData() { - return this[nodeSymbol].propsData; - } - - /** - * 获取符合搭建协议-节点 schema 结构 - */ - get schema(): any { - return this[nodeSymbol].schema; - } - - get settingEntry(): any { - return SettingTopEntry.create(this[nodeSymbol].settingEntry as any); - } - - /** - * @deprecated use .children instead - */ - getChildren() { - return this.children; - } - - /** - * 获取节点实例对应的 dom 节点 - */ - getDOMNode() { - return this[nodeSymbol].getDOMNode(); - } - - /** - * 执行新增、删除、排序等操作 - * @param remover - * @param adder - * @param sorter - */ - mergeChildren( - remover: (node: Node, idx: number) => boolean, - adder: (children: Node[]) => any, - sorter: (firstNode: Node, secondNode: Node) => number, - ) { - return this.children?.mergeChildren(remover, adder, sorter); - } - - /** - * 返回节点的尺寸、位置信息 - * @returns - */ - getRect() { - return this[nodeSymbol].getRect(); - } - - /** - * 是否有挂载插槽节点 - * @returns - */ - hasSlots() { - return this[nodeSymbol].hasSlots(); - } - - /** - * 是否设定了渲染条件 - * @returns - */ - hasCondition() { - return this[nodeSymbol].hasCondition(); - } - - /** - * 是否设定了循环数据 - * @returns - */ - hasLoop() { - return this[nodeSymbol].hasLoop(); - } - - getVisible() { - return this[nodeSymbol].getVisible(); - } - - setVisible(flag: boolean) { - this[nodeSymbol].setVisible(flag); - } - - isConditionalVisible() { - return this[nodeSymbol].isConditionalVisible(); - } - - /** - * @deprecated use .props instead - */ - getProps() { - return this.props; - } - - contains(node: Node) { - return this[nodeSymbol].contains(node[nodeSymbol]); - } - - /** - * 获取指定 path 的属性模型实例 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getProp(path: string, createIfNone = true): Prop | null { - return Prop.create(this[nodeSymbol].getProp(path, createIfNone)); - } - - /** - * 获取指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getPropValue(path: string) { - return this.getProp(path, false)?.getValue(); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraProp(path: string): Prop | null { - return Prop.create(this[nodeSymbol].getProp(getConvertedExtraKey(path))); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraPropValue(path: string) { - return this.getExtraProp(path)?.getValue(); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setPropValue(path: string, value: CompositeValue) { - return this.getProp(path)?.setValue(value); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setExtraPropValue(path: string, value: CompositeValue) { - return this.getExtraProp(path)?.setValue(value); - } - - /** - * 导入节点数据 - * @param data - */ - importSchema(data: NodeSchema) { - this[nodeSymbol].import(data); - } - - /** - * 导出节点数据 - * @param stage - * @param options - * @returns - */ - exportSchema(stage: TransformStage = TransformStage.Render, options?: any) { - return this[nodeSymbol].export(stage, options); - } - - /** - * 在指定位置之前插入一个节点 - * @param node - * @param ref - * @param useMutator - */ - insertBefore(node: Node, ref?: Node | undefined, useMutator?: boolean) { - this[nodeSymbol].insertBefore(node[nodeSymbol] || node, ref?.[nodeSymbol], useMutator); - } - - /** - * 在指定位置之后插入一个节点 - * @param node - * @param ref - * @param useMutator - */ - insertAfter(node: Node, ref?: Node | undefined, useMutator?: boolean) { - this[nodeSymbol].insertAfter(node[nodeSymbol] || node, ref?.[nodeSymbol], useMutator); - } - - /** - * 替换指定节点 - * @param node 待替换的子节点 - * @param data 用作替换的节点对象或者节点描述 - * @returns - */ - replaceChild(node: Node, data: any) { - return Node.create(this[nodeSymbol].replaceChild(node[nodeSymbol], data)); - } - - /** - * 将当前节点替换成指定节点描述 - * @param schema - */ - replaceWith(schema: NodeSchema) { - this[nodeSymbol].replaceWith(schema); - } - - /** - * 选中当前节点实例 - */ - select() { - this[nodeSymbol].select(); - } - - /** - * 设置悬停态 - * @param flag - */ - hover(flag = true) { - this[nodeSymbol].hover(flag); - } - - /** - * 删除当前节点实例 - */ - remove() { - this[nodeSymbol].remove(); - } -} diff --git a/packages/shell/src/project.ts b/packages/shell/src/project.ts deleted file mode 100644 index c6a29c4eb8..0000000000 --- a/packages/shell/src/project.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { - BuiltinSimulatorHost, - Project as InnerProject, - PropsReducer as PropsTransducer, - TransformStage, -} from '@alilc/lowcode-designer'; -import { RootSchema, ProjectSchema } from '@alilc/lowcode-types'; -import DocumentModel from './document-model'; -import SimulatorHost from './simulator-host'; -import { projectSymbol, simulatorHostSymbol, simulatorRendererSymbol, documentSymbol } from './symbols'; - -export default class Project { - private readonly [projectSymbol]: InnerProject; - private [simulatorHostSymbol]: BuiltinSimulatorHost; - private [simulatorRendererSymbol]: any; - - constructor(project: InnerProject) { - this[projectSymbol] = project; - } - - static create(project: InnerProject) { - return new Project(project); - } - - /** - * 获取当前的 document - * @returns - */ - get currentDocument(): DocumentModel | null { - return this.getCurrentDocument(); - } - - /** - * 获取当前 project 下所有 documents - * @returns - */ - get documents(): DocumentModel[] { - return this[projectSymbol].documents.map((doc) => DocumentModel.create(doc)!); - } - - /** - * 获取模拟器的 host - */ - get simulatorHost() { - return SimulatorHost.create(this[projectSymbol].simulator as any || this[simulatorHostSymbol]); - } - - /** - * @deprecated use .simulatorHost instead. - */ - get simulator() { - return this.simulatorHost; - } - - /** - * 打开一个 document - * @param doc - * @returns - */ - openDocument(doc?: string | RootSchema | undefined) { - const documentModel = this[projectSymbol].open(doc); - if (!documentModel) return null; - return DocumentModel.create(documentModel); - } - - /** - * 创建一个 document - * @param data - * @returns - */ - createDocument(data?: RootSchema): DocumentModel | null { - const doc = this[projectSymbol].createDocument(data); - return DocumentModel.create(doc); - } - - /** - * 删除一个 document - * @param doc - */ - removeDocument(doc: DocumentModel) { - this[projectSymbol].removeDocument(doc[documentSymbol]); - } - - /** - * 根据 fileName 获取 document - * @param fileName - * @returns - */ - getDocumentByFileName(fileName: string): DocumentModel | null { - return DocumentModel.create(this[projectSymbol].getDocumentByFileName(fileName)); - } - - /** - * 根据 id 获取 document - * @param id - * @returns - */ - getDocumentById(id: string): DocumentModel | null { - return DocumentModel.create(this[projectSymbol].getDocument(id)); - } - - /** - * 导出 project - * @returns - */ - exportSchema(stage: TransformStage = TransformStage.Render) { - return this[projectSymbol].getSchema(stage); - } - - /** - * 导入 project - * @param schema 待导入的 project 数据 - */ - importSchema(schema?: ProjectSchema) { - this[projectSymbol].load(schema, true); - } - - /** - * 获取当前的 document - * @returns - */ - getCurrentDocument(): DocumentModel | null { - return DocumentModel.create(this[projectSymbol].currentDocument); - } - - /** - * 增加一个属性的管道处理函数 - * @param transducer - * @param stage - */ - addPropsTransducer(transducer: PropsTransducer, stage: TransformStage) { - this[projectSymbol].designer.addPropsReducer(transducer, stage); - } - - /** - * 当前 project 内的 document 变更事件 - */ - onChangeDocument(fn: (doc: DocumentModel) => void) { - const offFn = this[projectSymbol].onCurrentDocumentChange((originalDoc) => { - fn(DocumentModel.create(originalDoc)!); - }); - if (this[projectSymbol].currentDocument) { - fn(DocumentModel.create(this[projectSymbol].currentDocument)!); - } - return offFn; - } - - /** - * 当前 project 的模拟器 ready 事件 - */ - onSimulatorHostReady(fn: (host: SimulatorHost) => void) { - const offFn = this[projectSymbol].onSimulatorReady((simulator: BuiltinSimulatorHost) => { - this[simulatorHostSymbol] = simulator; - fn(SimulatorHost.create(simulator)!); - }); - if (this[simulatorHostSymbol]) { - fn(SimulatorHost.create(this[simulatorHostSymbol])!); - } - return offFn; - } - - /** - * 当前 project 的渲染器 ready 事件 - */ - onSimulatorRendererReady(fn: () => void) { - const offFn = this[projectSymbol].onRendererReady((renderer: any) => { - this[simulatorRendererSymbol] = renderer; - fn(); - }); - if (this[simulatorRendererSymbol]) { - fn(); - } - return offFn; - } -} diff --git a/packages/shell/src/prop.ts b/packages/shell/src/prop.ts deleted file mode 100644 index aba59ca731..0000000000 --- a/packages/shell/src/prop.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Prop as InnerProp } from '@alilc/lowcode-designer'; -import { CompositeValue, TransformStage } from '@alilc/lowcode-types'; -import { propSymbol } from './symbols'; -import Node from './node'; - -export default class Prop { - private readonly [propSymbol]: InnerProp; - - constructor(prop: InnerProp) { - this[propSymbol] = prop; - } - - static create(prop: InnerProp | undefined | null) { - if (!prop) return null; - return new Prop(prop); - } - - /** - * id - */ - get id() { - return this[propSymbol].id; - } - - /** - * key 值 - */ - get key() { - return this[propSymbol].key; - } - - /** - * 返回当前 prop 的路径 - */ - get path() { - return this[propSymbol].path; - } - - /** - * 返回所属的节点实例 - */ - get node(): Node | null { - return Node.create(this[propSymbol].getNode()); - } - - /** - * return the slot node (only if the current prop represents a slot) - */ - get slotNode(): Node | null { - return Node.create(this[propSymbol].slotNode); - } - - /** - * judge if it is a prop or not - */ - get isProp() { return true; } - - /** - * 设置值 - * @param val - */ - setValue(val: CompositeValue) { - this[propSymbol].setValue(val); - } - - /** - * 获取值 - * @returns - */ - getValue() { - return this[propSymbol].getValue(); - } - - /** - * 导出值 - * @param stage - * @returns - */ - exportSchema(stage: TransformStage = TransformStage.Render) { - return this[propSymbol].export(stage); - } -} \ No newline at end of file diff --git a/packages/shell/src/props.ts b/packages/shell/src/props.ts deleted file mode 100644 index 293fa60df8..0000000000 --- a/packages/shell/src/props.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Props as InnerProps, getConvertedExtraKey } from '@alilc/lowcode-designer'; -import { CompositeValue, TransformStage } from '@alilc/lowcode-types'; -import { propsSymbol } from './symbols'; -import Node from './node'; -import Prop from './prop'; - -export default class Props { - private readonly [propsSymbol]: InnerProps; - - constructor(props: InnerProps) { - this[propsSymbol] = props; - } - - static create(props: InnerProps | undefined | null) { - if (!props) return null; - return new Props(props); - } - - /** - * id - */ - get id() { - return this[propsSymbol].id; - } - - /** - * 返回当前 props 的路径 - */ - get path() { - return this[propsSymbol].path; - } - - /** - * 返回所属的 node 实例 - */ - get node(): Node | null { - return Node.create(this[propsSymbol].getNode()); - } - - /** - * 获取指定 path 的属性模型实例 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getProp(path: string): Prop | null { - return Prop.create(this[propsSymbol].getProp(path)); - } - - /** - * 获取指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getPropValue(path: string) { - return this.getProp(path)?.getValue(); - } - - /** - * 获取指定 path 的属性模型实例, - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraProp(path: string): Prop | null { - return Prop.create(this[propsSymbol].getProp(getConvertedExtraKey(path))); - } - - /** - * 获取指定 path 的属性模型实例值 - * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @returns - */ - getExtraPropValue(path: string) { - return this.getExtraProp(path)?.getValue(); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setPropValue(path: string, value: CompositeValue) { - return this.getProp(path)?.setValue(value); - } - - /** - * 设置指定 path 的属性模型实例值 - * @param path 属性路径,支持 a / a.b / a.0 等格式 - * @param value 值 - * @returns - */ - setExtraPropValue(path: string, value: CompositeValue) { - return this.getExtraProp(path)?.setValue(value); - } - - /** - * test if the specified key is existing or not. - * @param key - * @returns - */ - has(key: string) { - return this[propsSymbol].has(key); - } - - /** - * add a key with given value - * @param value - * @param key - * @returns - */ - add(value: CompositeValue, key?: string | number | undefined) { - return this[propsSymbol].add(value, key); - } -} \ No newline at end of file diff --git a/packages/shell/src/selection.ts b/packages/shell/src/selection.ts deleted file mode 100644 index 8e29384d08..0000000000 --- a/packages/shell/src/selection.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - DocumentModel as InnerDocumentModel, - Node as InnerNode, - Selection as InnerSelection, -} from '@alilc/lowcode-designer'; -import Node from './node'; -import { documentSymbol, selectionSymbol } from './symbols'; - -export default class Selection { - private readonly [documentSymbol]: InnerDocumentModel; - private readonly [selectionSymbol]: InnerSelection; - - constructor(document: InnerDocumentModel) { - this[documentSymbol] = document; - this[selectionSymbol] = document.selection; - } - - /** - * 返回选中的节点 id - */ - get selected(): string[] { - return this[selectionSymbol].selected; - } - - /** - * return selected Node instance - */ - get node(): Node { - return this.getNodes()[0]; - } - - /** - * 选中指定节点(覆盖方式) - * @param id - */ - select(id: string) { - this[selectionSymbol].select(id); - } - - /** - * 批量选中指定节点们 - * @param ids - */ - selectAll(ids: string[]) { - this[selectionSymbol].selectAll(ids); - } - - /** - * 移除选中的指定节点 - * @param id - */ - remove(id: string) { - this[selectionSymbol].remove(id); - } - - /** - * 清除所有选中节点 - */ - clear() { - this[selectionSymbol].clear(); - } - - /** - * 判断是否选中了指定节点 - * @param id - * @returns - */ - has(id: string) { - return this[selectionSymbol].has(id); - } - - /** - * 选中指定节点(增量方式) - * @param id - */ - add(id: string) { - this[selectionSymbol].add(id); - } - - /** - * 获取选中的节点实例 - * @returns - */ - getNodes(): Node[] { - return this[selectionSymbol].getNodes().map((node: InnerNode) => Node.create(node)); - } -} diff --git a/packages/shell/src/setters.ts b/packages/shell/src/setters.ts deleted file mode 100644 index 000213883a..0000000000 --- a/packages/shell/src/setters.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { getSetter, registerSetter, getSettersMap, RegisteredSetter } from '@alilc/lowcode-editor-core'; -import { CustomView } from '@alilc/lowcode-types'; - -export default class Setters { - /** - * 获取指定 setter - * @param type - * @returns - */ - getSetter(type: string) { - return getSetter(type); - } - - /** - * 获取已注册的所有 settersMap - * @returns - */ - getSettersMap() { - return getSettersMap(); - } - - /** - * 注册一个 setter - * @param typeOrMaps - * @param setter - * @returns - */ - registerSetter( - typeOrMaps: string | { [key: string]: CustomView | RegisteredSetter }, - setter?: CustomView | RegisteredSetter | undefined, - ) { - return registerSetter(typeOrMaps, setter); - } -} diff --git a/packages/shell/src/setting-prop-entry.ts b/packages/shell/src/setting-prop-entry.ts deleted file mode 100644 index 399ce4c9c7..0000000000 --- a/packages/shell/src/setting-prop-entry.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { SettingField, ISetValueOptions } from '@alilc/lowcode-designer'; -import { CompositeValue, FieldConfig, CustomView, isCustomView } from '@alilc/lowcode-types'; -import { settingPropEntrySymbol } from './symbols'; -import Node from './node'; -import SettingTopEntry from './setting-top-entry'; -import ComponentMeta from './component-meta'; - -export default class SettingPropEntry { - private readonly [settingPropEntrySymbol]: SettingField; - - constructor(prop: SettingField) { - this[settingPropEntrySymbol] = prop; - } - - static create(prop: SettingField) { - return new SettingPropEntry(prop); - } - - /** - * 获取设置属性的 isGroup - */ - get isGroup() { - return this[settingPropEntrySymbol].isGroup; - } - - /** - * 获取设置属性的 id - */ - get id() { - return this[settingPropEntrySymbol].id; - } - - /** - * 获取设置属性的 name - */ - get name() { - return this[settingPropEntrySymbol].name; - } - - /** - * 获取设置属性的 key - */ - get key() { - return this[settingPropEntrySymbol].getKey(); - } - - /** - * 获取设置属性的 path - */ - get path() { - return this[settingPropEntrySymbol].path; - } - - /** - * 获取设置属性的 title - */ - get title() { - return this[settingPropEntrySymbol].title; - } - - /** - * 获取设置属性的 setter - */ - get setter() { - return this[settingPropEntrySymbol].setter; - } - - /** - * 获取设置属性的 expanded - */ - get expanded() { - return this[settingPropEntrySymbol].expanded; - } - - /** - * 获取设置属性的 extraProps - */ - get extraProps() { - return this[settingPropEntrySymbol].extraProps; - } - - get props() { - return SettingTopEntry.create(this[settingPropEntrySymbol].props); - } - - /** - * 获取设置属性对应的节点实例 - */ - get node(): Node | null { - return Node.create(this[settingPropEntrySymbol].getNode()); - } - - /** - * 获取设置属性的父设置属性 - */ - get parent(): SettingPropEntry { - return SettingPropEntry.create(this[settingPropEntrySymbol].parent as any); - } - - /** - * 是否是 SettingField 实例 - */ - get isSettingField(): boolean { - return this[settingPropEntrySymbol].isSettingField; - } - - /** - * componentMeta - */ - get componentMeta(): ComponentMeta | null { - return ComponentMeta.create(this[settingPropEntrySymbol].componentMeta); - } - - /** - * 获取设置属性的 items - */ - get items(): Array<SettingPropEntry | CustomView> { - return this[settingPropEntrySymbol].items?.map((item) => { - if (isCustomView(item)) { - return item; - } - return item.internalToShellPropEntry(); - }); - } - - /** - * 设置 key 值 - * @param key - */ - setKey(key: string | number) { - this[settingPropEntrySymbol].setKey(key); - } - - /** - * @deprecated use .node instead - */ - getNode() { - return this.node; - } - - /** - * @deprecated use .parent instead - */ - getParent() { - return this.parent; - } - - /** - * 设置值 - * @param val 值 - */ - setValue(val: CompositeValue, extraOptions?: ISetValueOptions) { - this[settingPropEntrySymbol].setValue(val, false, false, extraOptions); - } - - /** - * 设置子级属性值 - * @param propName 子属性名 - * @param value 值 - */ - setPropValue(propName: string | number, value: any) { - this[settingPropEntrySymbol].setPropValue(propName, value); - } - - /** - * 清空指定属性值 - * @param propName - */ - clearPropValue(propName: string | number) { - this[settingPropEntrySymbol].clearPropValue(propName); - } - - /** - * 获取配置的默认值 - * @returns - */ - getDefaultValue() { - return this[settingPropEntrySymbol].getDefaultValue(); - } - - /** - * 获取值 - * @returns - */ - getValue() { - return this[settingPropEntrySymbol].getValue(); - } - - /** - * 获取子级属性值 - * @param propName 子属性名 - * @returns - */ - getPropValue(propName: string | number) { - return this[settingPropEntrySymbol].getPropValue(propName); - } - - /** - * 获取顶层附属属性值 - */ - getExtraPropValue(propName: string) { - return this[settingPropEntrySymbol].getExtraPropValue(propName); - } - - /** - * 设置顶层附属属性值 - */ - setExtraPropValue(propName: string, value: any) { - this[settingPropEntrySymbol].setExtraPropValue(propName, value); - } - - /** - * 获取设置属性集 - * @returns - */ - getProps() { - return SettingTopEntry.create(this[settingPropEntrySymbol].getProps() as SettingEntry) as any; - } - - /** - * 是否绑定了变量 - * @returns - */ - isUseVariable() { - return this[settingPropEntrySymbol].isUseVariable(); - } - - /** - * 设置绑定变量 - * @param flag - */ - setUseVariable(flag: boolean) { - this[settingPropEntrySymbol].setUseVariable(flag); - } - - /** - * 创建一个设置 field 实例 - * @param config - * @returns - */ - createField(config: FieldConfig) { - return SettingPropEntry.create(this[settingPropEntrySymbol].createField(config)); - } - - /** - * 获取值,当为变量时,返回 mock - * @returns - */ - getMockOrValue() { - return this[settingPropEntrySymbol].getMockOrValue(); - } - - /** - * 销毁当前 field 实例 - */ - purge() { - this[settingPropEntrySymbol].purge(); - } - - /** - * 移除当前 field 实例 - */ - remove() { - this[settingPropEntrySymbol].remove(); - } - - /** - * 设置 autorun - * @param action - * @returns - */ - onEffect(action: () => void) { - return this[settingPropEntrySymbol].onEffect(action); - } - - /** - * 返回 shell 模型,兼容某些场景下 field 已经是 shell field 了 - * @returns - */ - internalToShellPropEntry() { - return this; - } -} diff --git a/packages/shell/src/setting-top-entry.ts b/packages/shell/src/setting-top-entry.ts deleted file mode 100644 index 6f5d888d42..0000000000 --- a/packages/shell/src/setting-top-entry.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { SettingEntry } from '@alilc/lowcode-designer'; -import { settingTopEntrySymbol } from './symbols'; -import Node from './node'; -import SettingPropEntry from './setting-prop-entry'; - -export default class SettingTopEntry { - private readonly [settingTopEntrySymbol]: SettingEntry; - - constructor(prop: SettingEntry) { - this[settingTopEntrySymbol] = prop; - } - - static create(prop: SettingEntry) { - return new SettingTopEntry(prop); - } - - /** - * 返回所属的节点实例 - */ - get node(): Node | null { - return Node.create(this[settingTopEntrySymbol].getNode()); - } - - /** - * 获取子级属性对象 - * @param propName - * @returns - */ - get(propName: string | number) { - return SettingPropEntry.create(this[settingTopEntrySymbol].get(propName) as any); - } - - /** - * @deprecated use .node instead - */ - getNode() { - return this.node; - } - - /** - * 获取指定 propName 的值 - * @param propName - * @returns - */ - getPropValue(propName: string | number) { - return this[settingTopEntrySymbol].getPropValue(propName); - } - - /** - * 设置指定 propName 的值 - * @param propName - * @param value - */ - setPropValue(propName: string | number, value: any) { - this[settingTopEntrySymbol].setPropValue(propName, value); - } -} \ No newline at end of file diff --git a/packages/shell/src/simulator-host.ts b/packages/shell/src/simulator-host.ts deleted file mode 100644 index 077564550a..0000000000 --- a/packages/shell/src/simulator-host.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - BuiltinSimulatorHost, -} from '@alilc/lowcode-designer'; -import { simulatorHostSymbol, nodeSymbol } from './symbols'; -import type Node from './node'; - -export default class SimulatorHost { - private readonly [simulatorHostSymbol]: BuiltinSimulatorHost; - - constructor(simulator: BuiltinSimulatorHost) { - this[simulatorHostSymbol] = simulator; - } - - static create(host: BuiltinSimulatorHost) { - if (!host) return null; - return new SimulatorHost(host); - } - - /** - * 获取 contentWindow - */ - get contentWindow() { - return this[simulatorHostSymbol].contentWindow; - } - - /** - * 获取 contentDocument - */ - get contentDocument() { - return this[simulatorHostSymbol].contentDocument; - } - - get renderer() { - return this[simulatorHostSymbol].renderer; - } - - /** - * 设置 host 配置值 - * @param key - * @param value - */ - set(key: string, value: any) { - this[simulatorHostSymbol].set(key, value); - } - - /** - * 获取 host 配置值 - * @param key - * @returns - */ - get(key: string) { - return this[simulatorHostSymbol].get(key); - } - - /** - * scroll to specific node - * @param node - */ - scrollToNode(node: Node) { - this[simulatorHostSymbol].scrollToNode(node[nodeSymbol]); - } - - /** - * 刷新渲染画布 - */ - rerender() { - this[simulatorHostSymbol].rerender(); - } -} diff --git a/packages/shell/src/skeleton.ts b/packages/shell/src/skeleton.ts deleted file mode 100644 index 2c8b178d5a..0000000000 --- a/packages/shell/src/skeleton.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { - Skeleton as InnerSkeleton, - IWidgetBaseConfig, - IWidgetConfigArea, - SkeletonEvents, -} from '@alilc/lowcode-editor-skeleton'; -import { skeletonSymbol } from './symbols'; - -export default class Skeleton { - private readonly [skeletonSymbol]: InnerSkeleton; - - constructor(skeleton: InnerSkeleton) { - this[skeletonSymbol] = skeleton; - } - - /** - * 增加一个面板实例 - * @param config - * @param extraConfig - * @returns - */ - add(config: IWidgetBaseConfig, extraConfig?: Record<string, any>) { - return this[skeletonSymbol].add(config, extraConfig); - } - - /** - * 移除一个面板实例 - * @param config - * @returns - */ - remove(config: IWidgetBaseConfig) { - const { area, name } = config; - const skeleton = this[skeletonSymbol]; - if (!normalizeArea(area)) return; - skeleton[normalizeArea(area)!].container.remove(name); - } - - /** - * 显示面板 - * @param name - */ - showPanel(name: string) { - this[skeletonSymbol].getPanel(name)?.show(); - } - - /** - * 隐藏面板 - * @param name - */ - hidePanel(name: string) { - this[skeletonSymbol].getPanel(name)?.hide(); - } - - /** - * 显示 widget - * @param name - */ - showWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.show(); - } - - /** - * enable widget - * @param name - */ - enableWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.enable?.(); - } - - /** - * 隐藏 widget - * @param name - */ - hideWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.hide(); - } - - /** - * disable widget,不可点击 - * @param name - */ - disableWidget(name: string) { - this[skeletonSymbol].getWidget(name)?.disable?.(); - } - - /** - * show area - * @param areaName name of area - */ - showArea(areaName: string) { - (this[skeletonSymbol] as any)[areaName]?.show(); - } - - /** - * hide area - * @param areaName name of area - */ - hideArea(areaName: string) { - (this[skeletonSymbol] as any)[areaName]?.hide(); - } - - /** - * 监听 panel 显示事件 - * @param listener - * @returns - */ - onShowPanel(listener: (...args: unknown[]) => void) { - const { editor } = this[skeletonSymbol]; - editor.on(SkeletonEvents.PANEL_SHOW, (name: any, panel: any) => { - // 不泄漏 skeleton - const { skeleton, ...restPanel } = panel; - listener(name, restPanel); - }); - return () => editor.off(SkeletonEvents.PANEL_SHOW, listener); - } - - /** - * 监听 panel 隐藏事件 - * @param listener - * @returns - */ - onHidePanel(listener: (...args: unknown[]) => void) { - const { editor } = this[skeletonSymbol]; - editor.on(SkeletonEvents.PANEL_HIDE, (name: any, panel: any) => { - // 不泄漏 skeleton - const { skeleton, ...restPanel } = panel; - listener(name, restPanel); - }); - return () => editor.off(SkeletonEvents.PANEL_HIDE, listener); - } - - /** - * 监听 widget 显示事件 - * @param listener - * @returns - */ - onShowWidget(listener: (...args: unknown[]) => void) { - const { editor } = this[skeletonSymbol]; - editor.on(SkeletonEvents.WIDGET_SHOW, (name: any, panel: any) => { - // 不泄漏 skeleton - const { skeleton, ...rest } = panel; - listener(name, rest); - }); - return () => editor.off(SkeletonEvents.WIDGET_SHOW, listener); - } - - /** - * 监听 widget 隐藏事件 - * @param listener - * @returns - */ - onHideWidget(listener: (...args: unknown[]) => void) { - const { editor } = this[skeletonSymbol]; - editor.on(SkeletonEvents.WIDGET_HIDE, (name: any, panel: any) => { - // 不泄漏 skeleton - const { skeleton, ...rest } = panel; - listener(name, rest); - }); - return () => editor.off(SkeletonEvents.WIDGET_HIDE, listener); - } -} - -function normalizeArea(area: IWidgetConfigArea | undefined) { - switch (area) { - case 'leftArea': - case 'left': - return 'leftArea'; - case 'rightArea': - case 'right': - return 'rightArea'; - case 'topArea': - case 'top': - return 'topArea'; - case 'toolbar': - return 'toolbar'; - case 'mainArea': - case 'main': - case 'center': - case 'centerArea': - return 'mainArea'; - case 'bottomArea': - case 'bottom': - return 'bottomArea'; - case 'leftFixedArea': - return 'leftFixedArea'; - case 'leftFloatArea': - return 'leftFloatArea'; - case 'stages': - return 'stages'; - default: - throw new Error(`${area} not supported`); - } -} diff --git a/packages/shell/src/symbols.ts b/packages/shell/src/symbols.ts index baeb4452ae..e0f846ad36 100644 --- a/packages/shell/src/symbols.ts +++ b/packages/shell/src/symbols.ts @@ -10,7 +10,7 @@ export const nodeSymbol = Symbol('node'); export const modalNodesManagerSymbol = Symbol('modalNodesManager'); export const nodeChildrenSymbol = Symbol('nodeChildren'); export const propSymbol = Symbol('prop'); -export const settingPropEntrySymbol = Symbol('settingPropEntry'); +export const settingFieldSymbol = Symbol('settingField'); export const settingTopEntrySymbol = Symbol('settingTopEntry'); export const propsSymbol = Symbol('props'); export const detectingSymbol = Symbol('detecting'); @@ -21,6 +21,23 @@ export const dragonSymbol = Symbol('dragon'); export const componentMetaSymbol = Symbol('componentMeta'); export const dropLocationSymbol = Symbol('dropLocation'); export const simulatorHostSymbol = Symbol('simulatorHost'); -export const simulatorRendererSymbol = Symbol('simulatorRenderer'); +export const simulatorRenderSymbol = Symbol('simulatorRender'); export const dragObjectSymbol = Symbol('dragObject'); -export const locateEventSymbol = Symbol('locateEvent'); \ No newline at end of file +export const locateEventSymbol = Symbol('locateEvent'); +export const designerCabinSymbol = Symbol('designerCabin'); +export const editorCabinSymbol = Symbol('editorCabin'); +export const skeletonCabinSymbol = Symbol('skeletonCabin'); +export const hotkeySymbol = Symbol('hotkey'); +export const pluginsSymbol = Symbol('plugins'); +export const workspaceSymbol = Symbol('workspace'); +export const windowSymbol = Symbol('window'); +export const pluginInstanceSymbol = Symbol('plugin-instance'); +export const resourceTypeSymbol = Symbol('resourceType'); +export const resourceSymbol = Symbol('resource'); +export const clipboardSymbol = Symbol('clipboard'); +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'); +export const commandSymbol = Symbol('command'); \ No newline at end of file diff --git a/packages/types/build.json b/packages/types/build.json index bd5cf18dde..3e92600554 100644 --- a/packages/types/build.json +++ b/packages/types/build.json @@ -1,5 +1,5 @@ { "plugins": [ - "build-plugin-component" + "@alilc/build-plugin-lce" ] } diff --git a/packages/types/package.json b/packages/types/package.json index af5ed0b26c..5651d427d4 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-types", - "version": "1.0.15", + "version": "1.3.2", "description": "Types for Ali lowCode engine", "files": [ "es", @@ -9,7 +9,7 @@ "main": "lib/index.js", "module": "es/index.js", "scripts": { - "build": "build-scripts build --skip-demo" + "build": "build-scripts build" }, "dependencies": { "@alilc/lowcode-datasource-types": "^1.0.0", @@ -19,8 +19,7 @@ "devDependencies": { "@alib/build-scripts": "^0.1.18", "@types/node": "^13.7.1", - "@types/react": "^16", - "build-plugin-component": "^0.2.10" + "@types/react": "^16" }, "publishConfig": { "access": "public", @@ -30,5 +29,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/types" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/types/src/activity.ts b/packages/types/src/activity.ts index 7491f9698f..8549df7b3d 100644 --- a/packages/types/src/activity.ts +++ b/packages/types/src/activity.ts @@ -1,4 +1,4 @@ -import { NodeSchema } from './schema'; +import { IPublicTypeNodeSchema } from './shell'; export enum ActivityType { 'ADDED' = 'added', @@ -7,8 +7,8 @@ export enum ActivityType { 'COMPOSITE' = 'composite', } -export interface IActivityPayload { - schema: NodeSchema; +interface IActivityPayload { + schema: IPublicTypeNodeSchema; location?: { parent: { nodeId: string; @@ -20,6 +20,10 @@ export interface IActivityPayload { newValue: any; } +/** + * TODO: not sure if this is used anywhere + * @deprecated + */ export type ActivityData = { type: ActivityType; payload: IActivityPayload; diff --git a/packages/types/src/app-config.ts b/packages/types/src/app-config.ts deleted file mode 100644 index 6343645c96..0000000000 --- a/packages/types/src/app-config.ts +++ /dev/null @@ -1,19 +0,0 @@ -export interface AppConfig { - sdkVersion?: string; - historyMode?: string; - targetRootID?: string; - layout?: Layout; - theme?: Theme; - [key: string]: any; -} - -interface Theme { - package: string; - version: string; - primary: string; -} - -interface Layout { - componentName?: string; - props?: Record<string, any>; -} diff --git a/packages/types/src/assets.ts b/packages/types/src/assets.ts index 37f5634299..f0e6d35396 100644 --- a/packages/types/src/assets.ts +++ b/packages/types/src/assets.ts @@ -1,14 +1,3 @@ -import { Snippet, ComponentMetadata } from './metadata'; -import { I18nData } from './i18n'; - -export interface AssetItem { - type: AssetType; - content?: string | null; - device?: string; - level?: AssetLevel; - id?: string; -} - export enum AssetLevel { // 环境依赖库 比如 react, react-dom Environment = 1, @@ -43,181 +32,21 @@ export enum AssetType { Bundle = 'bundle', } -export interface AssetBundle { - type: AssetType.Bundle; +export interface AssetItem { + type: AssetType; + content?: string | null; + device?: string; level?: AssetLevel; - assets?: Asset | AssetList | null; + id?: string; + scriptType?: string; } -export type Asset = AssetList | AssetBundle | AssetItem | URL; - export type AssetList = Array<Asset | undefined | null>; -/** - * 资产包协议 - */ -export interface AssetsJson { - /** - * 资产包协议版本号 - */ - version: string; - /** - * 大包列表,external与package的概念相似,融合在一起 - */ - packages?: Package[]; - /** - * 所有组件的描述协议列表所有组件的列表 - */ - components: Array<ComponentDescription | RemoteComponentDescription>; - /** - * 组件分类列表,用来描述物料面板 - * @deprecated 最新版物料面板已不需要此描述 - */ - componentList?: ComponentCategory[]; - /** - * 业务组件分类列表,用来描述物料面板 - * @deprecated 最新版物料面板已不需要此描述 - */ - bizComponentList?: ComponentCategory[]; - /** - * 用于描述组件面板中的 tab 和 category - */ - sort?: ComponentSort; -} - -/** - * 用于描述组件面板中的 tab 和 category - */ -export interface ComponentSort { - /** - * 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"] - */ - groupList?: string[]; - /** - * 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列; - */ - categoryList?: string[]; -} - -/** - * 定义组件大包及 external 资源的信息 - * 应该被编辑器默认加载 - */ -export interface Package { - /** - * 包名 - */ - package: string; - /** - * 包版本号 - */ - version: string; - /** - * 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css - */ - urls?: string[] | any; - /** - * 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css - */ - editUrls?: string[] | any; - /** - * 作为全局变量引用时的名称,和webpack output.library字段含义一样,用来定义全局变量名 - */ - library: string; - /** - * @experimental - * - * @todo 需推进提案 @度城 - */ - async?: boolean; - /** - * 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; - */ - exportName?: string; -} - -/** - * 组件分类 - * @deprecated 已被 ComponentMetadata 替代 - */ -export interface ComponentCategory { - /** - * 组件分类title - */ - title: string; - /** - * 组件分类icon - */ - icon?: string; - /** - * 可能有子分类 - */ - children?: ComponentItem[] | ComponentCategory[]; -} - -/** - * 组件 - * @deprecated 已被 ComponentMetadata 替代 - */ -export interface ComponentItem { - /** - * 组件title - */ - title: string; - /** - * 组件名 - */ - componentName?: string; - /** - * 组件icon - */ - icon?: string; - /** - * 可用片段 - */ - snippets?: Snippet[]; - /** - * 一级分组 - */ - group?: string | I18nData; - - /** - * 二级分组 - */ - category?: string | I18nData; - - /** - * 组件优先级排序 - */ - priority?: number; -} - -/** - * 本地物料描述 - */ -export interface ComponentDescription extends ComponentMetadata { - /** - * @todo 待补充文档 @jinchan - */ - keywords: string[]; -} +export type Asset = AssetList | AssetBundle | AssetItem | URL; -/** - * 远程物料描述 - */ -export interface RemoteComponentDescription { - /** - * 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; - */ - exportName?: string; - /** - * 组件描述的资源链接; - */ - url?: string; - /** - * 组件(库)的 npm 信息; - */ - package?: { - npm?: string; - }; +export interface AssetBundle { + type: AssetType.Bundle; + level?: AssetLevel; + assets?: Asset | AssetList | null; } diff --git a/packages/types/src/deprecated/index.ts b/packages/types/src/deprecated/index.ts new file mode 100644 index 0000000000..7e65173c0d --- /dev/null +++ b/packages/types/src/deprecated/index.ts @@ -0,0 +1,18 @@ +export * from './isActionContentObject'; +export * from './isCustomView'; +export * from './isDOMText'; +export * from './isDynamicSetter'; +export * from './isI18nData'; +export * from './isJSBlock'; +export * from './isJSExpression'; +export * from './isJSFunction'; +export * from './isJSSlot'; +export * from './isLowCodeComponentType'; +export * from './isNodeSchema'; +export * from './isPlainObject'; +export * from './isProCodeComponentType'; +export * from './isProjectSchema'; +export * from './isReactClass'; +export * from './isReactComponent'; +export * from './isSetterConfig'; +export * from './isTitleConfig'; \ No newline at end of file diff --git a/packages/types/src/deprecated/isActionContentObject.ts b/packages/types/src/deprecated/isActionContentObject.ts new file mode 100644 index 0000000000..88a8e57d2e --- /dev/null +++ b/packages/types/src/deprecated/isActionContentObject.ts @@ -0,0 +1,8 @@ +import { IPublicTypeActionContentObject } from '../shell'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isActionContentObject(obj: any): obj is IPublicTypeActionContentObject { + return obj && typeof obj === 'object'; +} diff --git a/packages/types/src/deprecated/isCustomView.ts b/packages/types/src/deprecated/isCustomView.ts new file mode 100644 index 0000000000..159490e550 --- /dev/null +++ b/packages/types/src/deprecated/isCustomView.ts @@ -0,0 +1,10 @@ +import { isValidElement } from 'react'; +import { isReactComponent } from './isReactComponent'; +import { IPublicTypeCustomView } from '../shell/type/custom-view'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isCustomView(obj: any): obj is IPublicTypeCustomView { + return obj && (isValidElement(obj) || isReactComponent(obj)); +} diff --git a/packages/types/src/deprecated/isDOMText.ts b/packages/types/src/deprecated/isDOMText.ts new file mode 100644 index 0000000000..4ddc91320f --- /dev/null +++ b/packages/types/src/deprecated/isDOMText.ts @@ -0,0 +1,8 @@ +import { IPublicTypeDOMText } from '../shell/type/dom-text'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isDOMText(data: any): data is IPublicTypeDOMText { + return typeof data === 'string'; +} diff --git a/packages/types/src/deprecated/isDynamicSetter.ts b/packages/types/src/deprecated/isDynamicSetter.ts new file mode 100644 index 0000000000..55532d258d --- /dev/null +++ b/packages/types/src/deprecated/isDynamicSetter.ts @@ -0,0 +1,9 @@ +import { isReactClass } from './isReactClass'; +import { IPublicTypeDynamicSetter } from '../shell/type/dynamic-setter'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isDynamicSetter(obj: any): obj is IPublicTypeDynamicSetter { + return obj && typeof obj === 'function' && !isReactClass(obj); +} diff --git a/packages/types/src/deprecated/isI18nData.ts b/packages/types/src/deprecated/isI18nData.ts new file mode 100644 index 0000000000..4767ccd373 --- /dev/null +++ b/packages/types/src/deprecated/isI18nData.ts @@ -0,0 +1,7 @@ + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isI18nData(obj: any): boolean { + return obj && obj.type === 'i18n'; +} diff --git a/packages/types/src/deprecated/isJSBlock.ts b/packages/types/src/deprecated/isJSBlock.ts new file mode 100644 index 0000000000..6f92e2fcf1 --- /dev/null +++ b/packages/types/src/deprecated/isJSBlock.ts @@ -0,0 +1,8 @@ +import { IPublicTypeJSBlock } from '../shell/type/value-type'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isJSBlock(data: any): data is IPublicTypeJSBlock { + return data && data.type === 'JSBlock'; +} diff --git a/packages/types/src/deprecated/isJSExpression.ts b/packages/types/src/deprecated/isJSExpression.ts new file mode 100644 index 0000000000..f722d55293 --- /dev/null +++ b/packages/types/src/deprecated/isJSExpression.ts @@ -0,0 +1,8 @@ +import { IPublicTypeJSExpression } from '../shell/type/value-type'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isJSExpression(data: any): data is IPublicTypeJSExpression { + return data && data.type === 'JSExpression' && data.extType !== 'function'; +} diff --git a/packages/types/src/deprecated/isJSFunction.ts b/packages/types/src/deprecated/isJSFunction.ts new file mode 100644 index 0000000000..40ab4f52dc --- /dev/null +++ b/packages/types/src/deprecated/isJSFunction.ts @@ -0,0 +1,8 @@ +import { IPublicTypeJSFunction } from '../shell/type/value-type'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isJSFunction(x: any): x is IPublicTypeJSFunction { + return typeof x === 'object' && x && x.type === 'JSFunction'; +} diff --git a/packages/types/src/deprecated/isJSSlot.ts b/packages/types/src/deprecated/isJSSlot.ts new file mode 100644 index 0000000000..7cba651958 --- /dev/null +++ b/packages/types/src/deprecated/isJSSlot.ts @@ -0,0 +1,8 @@ +import { IPublicTypeJSSlot } from '../shell/type/value-type'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isJSSlot(data: any): data is IPublicTypeJSSlot { + return data && data.type === 'JSSlot'; +} diff --git a/packages/types/src/deprecated/isLowCodeComponentType.ts b/packages/types/src/deprecated/isLowCodeComponentType.ts new file mode 100644 index 0000000000..c14c85f1eb --- /dev/null +++ b/packages/types/src/deprecated/isLowCodeComponentType.ts @@ -0,0 +1,9 @@ +import { isProCodeComponentType } from './isProCodeComponentType'; +import { IPublicTypeComponentMap, IPublicTypeLowCodeComponent } from '../shell/type/npm'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isLowCodeComponentType(desc: IPublicTypeComponentMap): desc is IPublicTypeLowCodeComponent { + return !isProCodeComponentType(desc); +} diff --git a/packages/types/src/deprecated/isNodeSchema.ts b/packages/types/src/deprecated/isNodeSchema.ts new file mode 100644 index 0000000000..cab4dc46e1 --- /dev/null +++ b/packages/types/src/deprecated/isNodeSchema.ts @@ -0,0 +1,8 @@ +import { IPublicTypeNodeSchema } from '../shell'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isNodeSchema(data: any): data is IPublicTypeNodeSchema { + return data && data.componentName; +} diff --git a/packages/types/src/deprecated/isPlainObject.ts b/packages/types/src/deprecated/isPlainObject.ts new file mode 100644 index 0000000000..549f497360 --- /dev/null +++ b/packages/types/src/deprecated/isPlainObject.ts @@ -0,0 +1,10 @@ +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isPlainObject(value: any): value is Record<string, unknown> { + if (typeof value !== 'object') { + return false; + } + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null; +} diff --git a/packages/types/src/deprecated/isProCodeComponentType.ts b/packages/types/src/deprecated/isProCodeComponentType.ts new file mode 100644 index 0000000000..40e8e977f9 --- /dev/null +++ b/packages/types/src/deprecated/isProCodeComponentType.ts @@ -0,0 +1,8 @@ +import { IPublicTypeComponentMap, IPublicTypeProCodeComponent } from '../shell/type/npm'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isProCodeComponentType(desc: IPublicTypeComponentMap): desc is IPublicTypeProCodeComponent { + return 'package' in desc; +} diff --git a/packages/types/src/deprecated/isProjectSchema.ts b/packages/types/src/deprecated/isProjectSchema.ts new file mode 100644 index 0000000000..1622fa8466 --- /dev/null +++ b/packages/types/src/deprecated/isProjectSchema.ts @@ -0,0 +1,6 @@ +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isProjectSchema(data: any): boolean { + return data && data.componentsTree; +} diff --git a/packages/types/src/deprecated/isReactClass.ts b/packages/types/src/deprecated/isReactClass.ts new file mode 100644 index 0000000000..846c522d7b --- /dev/null +++ b/packages/types/src/deprecated/isReactClass.ts @@ -0,0 +1,8 @@ +import { ComponentClass, Component } from 'react'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isReactClass(obj: any): obj is ComponentClass<any> { + return obj && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component); +} diff --git a/packages/types/src/deprecated/isReactComponent.ts b/packages/types/src/deprecated/isReactComponent.ts new file mode 100644 index 0000000000..1ed04427f3 --- /dev/null +++ b/packages/types/src/deprecated/isReactComponent.ts @@ -0,0 +1,9 @@ +import { ComponentType } from 'react'; +import { isReactClass } from './isReactClass'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isReactComponent(obj: any): obj is ComponentType<any> { + return obj && (isReactClass(obj) || typeof obj === 'function'); +} diff --git a/packages/types/src/deprecated/isSetterConfig.ts b/packages/types/src/deprecated/isSetterConfig.ts new file mode 100644 index 0000000000..bf0d77e115 --- /dev/null +++ b/packages/types/src/deprecated/isSetterConfig.ts @@ -0,0 +1,9 @@ +import { IPublicTypeSetterConfig } from '../shell/type/setter-config'; +import { isCustomView } from './isCustomView'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isSetterConfig(obj: any): obj is IPublicTypeSetterConfig { + return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); +} diff --git a/packages/types/src/deprecated/isTitleConfig.ts b/packages/types/src/deprecated/isTitleConfig.ts new file mode 100644 index 0000000000..9ee38c9c25 --- /dev/null +++ b/packages/types/src/deprecated/isTitleConfig.ts @@ -0,0 +1,10 @@ +import { isI18nData } from './isI18nData'; +import { isPlainObject } from './isPlainObject'; +import { IPublicTypeTitleConfig } from '../shell/type/title-config'; + +/** + * @deprecated use same function from '@alilc/lowcode-utils' instead + */ +export function isTitleConfig(obj: any): obj is IPublicTypeTitleConfig { + return isPlainObject(obj) && !isI18nData(obj); +} diff --git a/packages/types/src/disposable.ts b/packages/types/src/disposable.ts deleted file mode 100644 index 1743677f18..0000000000 --- a/packages/types/src/disposable.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Disposable { - (): void; -} \ No newline at end of file diff --git a/packages/types/src/editor.ts b/packages/types/src/editor.ts index 7db0a5990b..3691a7f948 100644 --- a/packages/types/src/editor.ts +++ b/packages/types/src/editor.ts @@ -1,57 +1,5 @@ -import { EventEmitter } from 'events'; -import StrictEventEmitter from 'strict-event-emitter-types'; import { ReactNode, ComponentType } from 'react'; -import { NpmInfo } from './npm'; -import * as GlobalEvent from './event'; - -export type KeyType = (new (...args: any[]) => any) | symbol | string; -export type ClassType = new (...args: any[]) => any; -export interface GetOptions { - forceNew?: boolean; - sourceCls?: ClassType; -} -export type GetReturnType<T, ClsType> = T extends undefined - ? ClsType extends { - prototype: infer R; - } - ? R - : any - : T; - -/** - * duck-typed power-di - * - * @see https://www.npmjs.com/package/power-di - */ -interface PowerDIRegisterOptions { - /** default: true */ - singleton?: boolean; - /** if data a class, auto new a instance. - * if data a function, auto run(lazy). - * default: true */ - autoNew?: boolean; -} - -export interface IEditor extends StrictEventEmitter<EventEmitter, GlobalEvent.EventConfig> { - get: <T = undefined, KeyOrType = any>( - keyOrType: KeyOrType, - opt?: GetOptions - ) => GetReturnType<T, KeyOrType> | undefined; - - has: (keyOrType: KeyType) => boolean; - - set: (key: KeyType, data: any) => void | Promise<void>; - - onceGot: <T = undefined, KeyOrType extends KeyType = any> - (keyOrType: KeyOrType) => Promise<GetReturnType<T, KeyOrType>>; - - onGot: <T = undefined, KeyOrType extends KeyType = any>( - keyOrType: KeyOrType, - fn: (data: GetReturnType<T, KeyOrType>) => void, - ) => () => void; - - register: (data: any, key?: KeyType, options?: PowerDIRegisterOptions) => void; -} +import { IPublicTypeNpmInfo, IPublicModelEditor } from './shell'; export interface EditorConfig { skeleton?: SkeletonConfig; @@ -66,7 +14,7 @@ export interface EditorConfig { } export interface SkeletonConfig { - config: NpmInfo; + config: IPublicTypeNpmInfo; props?: Record<string, unknown>; handler?: (config: EditorConfig) => EditorConfig; } @@ -102,7 +50,7 @@ export interface PluginConfig { panelProps?: Record<string, unknown>; linkProps?: Record<string, unknown>; }; - config?: NpmInfo; + config?: IPublicTypeNpmInfo; pluginProps?: Record<string, unknown>; } @@ -111,14 +59,14 @@ export type HooksConfig = HookConfig[]; export interface HookConfig { message: string; type: 'on' | 'once'; - handler: (this: IEditor, editor: IEditor, ...args: any[]) => void; + handler: (this: IPublicModelEditor, editor: IPublicModelEditor, ...args: any[]) => void; } export type ShortCutsConfig = ShortCutConfig[]; export interface ShortCutConfig { keyboard: string; - handler: (editor: IEditor, ev: Event, keymaster: any) => void; + handler: (editor: IPublicModelEditor, ev: Event, keymaster: any) => void; } export type UtilsConfig = UtilConfig[]; @@ -126,14 +74,14 @@ export type UtilsConfig = UtilConfig[]; export interface UtilConfig { name: string; type: 'npm' | 'function'; - content: NpmInfo | ((...args: []) => any); + content: IPublicTypeNpmInfo | ((...args: []) => any); } export type ConstantsConfig = Record<string, unknown>; export interface LifeCyclesConfig { - init?: (editor: IEditor) => any; - destroy?: (editor: IEditor) => any; + init?: (editor: IPublicModelEditor) => any; + destroy?: (editor: IPublicModelEditor) => any; } export type LocaleType = 'zh-CN' | 'zh-TW' | 'en-US' | 'ja-JP'; @@ -156,7 +104,7 @@ export interface Utils { } export interface PluginProps { - editor: IEditor; + editor?: IPublicModelEditor; config: PluginConfig; [key: string]: any; } @@ -176,7 +124,7 @@ export interface PluginSet { } export type PluginClass = ComponentType<PluginProps> & { - init?: (editor: IEditor) => void; + init?: (editor: IPublicModelEditor) => void; defaultProps?: { locale?: LocaleType; messages?: I18nMessages; @@ -196,4 +144,4 @@ export interface PluginStatus { export interface PluginStatusSet { [key: string]: PluginStatus; -} +} \ No newline at end of file diff --git a/packages/types/src/field-config.ts b/packages/types/src/field-config.ts deleted file mode 100644 index 70a881fde7..0000000000 --- a/packages/types/src/field-config.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { TitleContent } from './title'; -import { SetterType, DynamicSetter } from './setter-config'; -import { SettingTarget } from './setting-target'; -import { LiveTextEditingConfig } from './metadata'; - -/** - * extra props for field - */ -export interface FieldExtraProps { - /** - * 是否必填参数 - */ - isRequired?: boolean; - /** - * default value of target prop for setter use - */ - defaultValue?: any; - /** - * get value for field - */ - getValue?: (target: SettingTarget, fieldValue: any) => any; - /** - * set value for field - */ - setValue?: (target: SettingTarget, value: any) => void; - /** - * the field conditional show, is not set always true - * @default undefined - */ - condition?: (target: SettingTarget) => boolean; - /** - * autorun when something change - */ - autorun?: (target: SettingTarget) => void; - /** - * is this field is a virtual field that not save to schema - */ - virtual?: (target: SettingTarget) => boolean; - /** - * default collapsed when display accordion - */ - defaultCollapsed?: boolean; - /** - * important field - */ - important?: boolean; - /** - * internal use - */ - forceInline?: number; - /** - * 是否支持变量配置 - */ - supportVariable?: boolean; - /** - * compatiable vision display - */ - display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; - // @todo 这个 omit 是否合理? - /** - * @todo 待补充文档 - */ - liveTextEditing?: Omit<LiveTextEditingConfig, 'propTarget'>; -} - -/** - * 属性面板配置 - */ -export interface FieldConfig extends FieldExtraProps { - /** - * 面板配置隶属于单个 field 还是分组 - */ - type?: 'field' | 'group'; - /** - * the name of this setting field, which used in quickEditor - */ - name: string | number; - /** - * the field title - * @default sameas .name - */ - title?: TitleContent; - /** - * 单个属性的 setter 配置 - * - * the field body contains when .type = 'field' - */ - setter?: SetterType | DynamicSetter; - /** - * the setting items which group body contains when .type = 'group' - */ - items?: FieldConfig[]; - /** - * extra props for field - * 其他配置属性(不做流通要求) - */ - extraProps?: FieldExtraProps; - /** - * @deprecated - */ - description?: TitleContent; - /** - * @deprecated - */ - isExtends?: boolean; -} diff --git a/packages/types/src/i18n.ts b/packages/types/src/i18n.ts deleted file mode 100644 index 44f8ef8550..0000000000 --- a/packages/types/src/i18n.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ReactNode } from 'react'; - -export interface I18nData { - type: 'i18n'; - intl?: ReactNode; - [key: string]: any; -} - -// type checks -export function isI18nData(obj: any): obj is I18nData { - return obj && obj.type === 'i18n'; -} - -export interface I18nMap { - [lang: string]: { [key: string]: string }; -} diff --git a/packages/types/src/icon.ts b/packages/types/src/icon.ts deleted file mode 100644 index 2f2ba23360..0000000000 --- a/packages/types/src/icon.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ReactElement, ComponentType } from 'react'; - -export interface IconConfig { - type: string; - size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; - className?: string; -} - -export type IconType = string | ReactElement | ComponentType<any> | IconConfig; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 19d9bfa709..d14dc9b995 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1,23 +1,11 @@ export * from '@alilc/lowcode-datasource-types'; export * from './editor'; -export * from './field-config'; -export * from './i18n'; -export * from './icon'; -export * from './metadata'; -export * from './npm'; -export * from './prop-config'; -export * from './schema'; export * from './activity'; -export * from './tip'; -export * from './title'; -export * from './utils'; -export * from './value-type'; -export * from './setter-config'; -export * from './setting-target'; -export * from './node'; -export * from './transform-stage'; export * from './code-intermediate'; export * from './code-result'; export * from './assets'; export * as GlobalEvent from './event'; -export * from './disposable'; +export * from './shell'; +export * from './shell-model-factory'; +// TODO: remove this in future versions +export * from './deprecated'; diff --git a/packages/types/src/metadata.ts b/packages/types/src/metadata.ts deleted file mode 100644 index dd93aea00f..0000000000 --- a/packages/types/src/metadata.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { ReactNode, ComponentType, ReactElement } from 'react'; -import { IconType } from './icon'; -import { TipContent } from './tip'; -import { TitleContent } from './title'; -import { PropConfig, PropType } from './prop-config'; -import { NpmInfo } from './npm'; -import { FieldConfig } from './field-config'; -import { NodeSchema, NodeData, ComponentSchema } from './schema'; -import { SettingTarget } from './setting-target'; -import { I18nData } from './i18n'; - -/** - * 嵌套控制函数 - */ -export type NestingFilter = (testNode: any, currentNode: any) => boolean; -/** - * 嵌套控制 - * 防止错误的节点嵌套,比如 a 嵌套 a, FormField 只能在 Form 容器下,Column 只能在 Table 下等 - */ -export interface NestingRule { - /** - * 子级白名单 - */ - childWhitelist?: string[] | string | RegExp | NestingFilter; - /** - * 父级白名单 - */ - parentWhitelist?: string[] | string | RegExp | NestingFilter; - /** - * 后裔白名单 - */ - descendantWhitelist?: string[] | string | RegExp | NestingFilter; - /** - * 后裔黑名单 - */ - descendantBlacklist?: string[] | string | RegExp | NestingFilter; - /** - * 祖先白名单 可用来做区域高亮 - */ - ancestorWhitelist?: string[] | string | RegExp | NestingFilter; -} - -/** - * 组件能力配置 - */ -export interface ComponentConfigure { - /** - * 是否容器组件 - */ - isContainer?: boolean; - /** - * 组件是否带浮层,浮层组件拖入设计器时会遮挡画布区域,此时应当辅助一些交互以防止阻挡 - */ - isModal?: boolean; - /** - * 是否存在渲染的根节点 - */ - isNullNode?: boolean; - /** - * 组件树描述信息 - */ - descriptor?: string; - /** - * 嵌套控制:防止错误的节点嵌套 - * 比如 a 嵌套 a, FormField 只能在 Form 容器下,Column 只能在 Table 下等 - */ - nestingRule?: NestingRule; - - /** - * 是否是最小渲染单元 - * 最小渲染单元下的组件渲染和更新都从单元的根节点开始渲染和更新。如果嵌套了多层最小渲染单元,渲染会从最外层的最小渲染单元开始渲染。 - */ - isMinimalRenderUnit?: boolean; - - /** - * 组件选中框的 cssSelector - */ - rootSelector?: string; - /** - * 禁用的行为,可以为 `'copy'`, `'move'`, `'remove'` 或它们组成的数组 - */ - disableBehaviors?: string[] | string; - /** - * 用于详细配置上述操作项的内容 - */ - actions?: ComponentAction[]; -} - -/** - * 可用片段 - * - * 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema - */ -export interface Snippet { - /** - * 组件分类title - */ - title?: string; - /** - * snippet 截图 - */ - screenshot?: string; - /** - * snippet 打标 - * - * @deprecated 暂未使用 - */ - label?: string; - /** - * 待插入的 schema - */ - schema?: NodeSchema; -} - -export interface InitialItem { - name: string; - initial: (target: SettingTarget, currentValue: any) => any; -} -export interface FilterItem { - name: string; - filter: (target: SettingTarget | null, currentValue: any) => any; -} -export interface AutorunItem { - name: string; - autorun: (target: SettingTarget) => any; -} - -/** - * 高级特性配置 - */ -export interface Advanced { - /** - * @todo 待补充文档 - */ - context?: { [contextInfoName: string]: any }; - /** - * @deprecated 使用组件 metadata 上的 snippets 字段即可 - */ - snippets?: Snippet[]; - /** - * @todo 待补充文档 - */ - view?: ComponentType<any>; - /** - * @todo 待补充文档 - */ - transducers?: any; - /** - * @deprecated 用于动态初始化拖拽到设计器里的组件的 prop 的值 - */ - initials?: InitialItem[]; - /** - * @todo 待补充文档 - */ - filters?: FilterItem[]; - /** - * @todo 待补充文档 - */ - autoruns?: AutorunItem[]; - /** - * 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 - */ - callbacks?: Callbacks; - /** - * 拖入容器时,自动带入 children 列表 - */ - initialChildren?: NodeData[] | ((target: SettingTarget) => NodeData[]); - /** - * @todo 待补充文档 - */ - isAbsoluteLayoutContainer?: boolean; - /** - * @todo 待补充文档 - */ - hideSelectTools?: boolean; - - /** - * 样式 及 位置,handle上必须有明确的标识以便事件路由判断,或者主动设置事件独占模式 - * NWSE 是交给引擎计算放置位置,ReactElement 必须自己控制初始位置 - */ - /** - * 用于配置设计器中组件 resize 操作工具的样式和内容 - * - hover 时控制柄高亮 - * - mousedown 时请求独占 - * - dragstart 请求通用 resizing 控制 请求 hud 显示 - * - drag 时 计算并设置效果,更新控制柄位置 - */ - getResizingHandlers?: ( - currentNode: any, - ) => ( - | Array<{ - type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; - content?: ReactElement; - propTarget?: string; - appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; - }> - | ReactElement[] - ); - - /** - * Live Text Editing:如果 children 内容是纯文本,支持双击直接编辑 - */ - liveTextEditing?: LiveTextEditingConfig[]; - - /** - * @deprecated 暂未使用 - */ - isTopFixed?: boolean; -} - -// thinkof Array -/** - * Live Text Editing(如果 children 内容是纯文本,支持双击直接编辑)的可配置项目 - */ -export interface LiveTextEditingConfig { - /** - * @todo 待补充文档 - */ - propTarget: string; - /** - * @todo 待补充文档 - */ - selector?: string; - /** - * 编辑模式 纯文本|段落编辑|文章编辑(默认纯文本,无跟随工具条) - * @default 'plaintext' - */ - mode?: 'plaintext' | 'paragraph' | 'article'; - /** - * 从 contentEditable 获取内容并设置到属性 - */ - onSaveContent?: (content: string, prop: any) => any; -} - -export type ConfigureSupportEvent = string | { - name: string; - propType?: PropType; - description?: string; -}; - -/** - * 通用扩展面板支持性配置 - */ -export interface ConfigureSupport { - /** - * 支持事件列表 - */ - events?: ConfigureSupportEvent[]; - /** - * 支持 className 设置 - */ - className?: boolean; - /** - * 支持样式设置 - */ - style?: boolean; - /** - * 支持生命周期设置 - */ - lifecycles?: any[]; - // general?: boolean; - /** - * 支持循环设置 - */ - loop?: boolean; - /** - * 支持条件式渲染设置 - */ - condition?: boolean; -} - -/** - * 编辑体验配置 - */ -export interface Configure { - /** - * 属性面板配置 - */ - props?: FieldConfig[]; - /** - * 组件能力配置 - */ - component?: ComponentConfigure; - /** - * 通用扩展面板支持性配置 - */ - supports?: ConfigureSupport; - /** - * 高级特性配置 - */ - advanced?: Advanced; -} - -/** - * 动作描述 - */ -export interface ActionContentObject { - /** - * 图标 - */ - icon?: IconType; - /** - * 描述 - */ - title?: TipContent; - /** - * 执行动作 - */ - action?: (currentNode: any) => void; -} - -/** - * @todo 工具条动作 - */ -export interface ComponentAction { - /** - * behaviorName - */ - name: string; - /** - * 菜单名称 - */ - content: string | ReactNode | ActionContentObject; - /** - * 子集 - */ - items?: ComponentAction[]; - /** - * 显示与否 - * always: 无法禁用 - */ - condition?: boolean | ((currentNode: any) => boolean) | 'always'; - /** - * 显示在工具条上 - */ - important?: boolean; -} - -export function isActionContentObject(obj: any): obj is ActionContentObject { - return obj && typeof obj === 'object'; -} - -/** - * 组件 meta 配置 - */ -export interface ComponentMetadata { - /** - * 组件名 - */ - componentName: string; - /** - * unique id - */ - uri?: string; - /** - * title or description - */ - title?: TitleContent; - /** - * svg icon for component - */ - icon?: IconType; - /** - * 组件标签 - */ - tags?: string[]; - /** - * 组件描述 - */ - description?: string; - /** - * 组件文档链接 - */ - docUrl?: string; - /** - * 组件快照 - */ - screenshot?: string; - /** - * 组件研发模式 - */ - devMode?: 'proCode' | 'lowCode'; - /** - * npm 源引入完整描述对象 - */ - npm?: NpmInfo; - /** - * 组件属性信息 - */ - props?: PropConfig[]; - /** - * 编辑体验增强 - */ - configure?: FieldConfig[] | Configure; - /** - * @deprecated, use advanced instead - */ - experimental?: Advanced; - /** - * @todo 待补充文档 - */ - schema?: ComponentSchema; - /** - * 可用片段 - */ - snippets?: Snippet[]; - /** - * 一级分组 - */ - group?: string | I18nData; - /** - * 二级分组 - */ - category?: string | I18nData; - /** - * 组件优先级排序 - */ - priority?: number; -} - -/** - * @todo 待补充文档 - */ -export interface TransformedComponentMetadata extends ComponentMetadata { - configure: Configure & { combined?: FieldConfig[] }; -} - -/** - * handleResizing - */ - -/** - * 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 - */ -export interface Callbacks { - // hooks - onMouseDownHook?: (e: MouseEvent, currentNode: any) => any; - onDblClickHook?: (e: MouseEvent, currentNode: any) => any; - onClickHook?: (e: MouseEvent, currentNode: any) => any; - // onLocateHook?: (e: any, currentNode: any) => any; - // onAcceptHook?: (currentNode: any, locationData: any) => any; - onMoveHook?: (currentNode: any) => boolean; - // thinkof 限制性拖拽 - onHoverHook?: (currentNode: any) => boolean; - onChildMoveHook?: (childNode: any, currentNode: any) => boolean; - - // events - onNodeRemove?: (removedNode: any, currentNode: any) => void; - onNodeAdd?: (addedNode: any, currentNode: any) => void; - onSubtreeModified?: (currentNode: any, options: any) => void; - onResize?: ( - e: MouseEvent & { - trigger: string; - deltaX?: number; - deltaY?: number; - }, - currentNode: any, - ) => void; - onResizeStart?: ( - e: MouseEvent & { - trigger: string; - deltaX?: number; - deltaY?: number; - }, - currentNode: any, - ) => void; - onResizeEnd?: ( - e: MouseEvent & { - trigger: string; - deltaX?: number; - deltaY?: number; - }, - currentNode: any, - ) => void; -} diff --git a/packages/types/src/node.ts b/packages/types/src/node.ts deleted file mode 100644 index 400f6d9873..0000000000 --- a/packages/types/src/node.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface NodeStatus { - locking: boolean; - pseudo: boolean; - inPlaceEditing: boolean; -} diff --git a/packages/types/src/npm.ts b/packages/types/src/npm.ts deleted file mode 100644 index 88449f435c..0000000000 --- a/packages/types/src/npm.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * npm 源引入完整描述对象 - */ -export interface NpmInfo { - /** - * 源码组件名称 - */ - componentName?: string; - /** - * 源码组件库名 - */ - package: string; - /** - * 源码组件版本号 - */ - version?: string; - /** - * 是否解构 - */ - destructuring?: boolean; - /** - * 源码组件名称 - */ - exportName?: string; - /** - * 子组件名 - */ - subName?: string; - /** - * 组件路径 - */ - main?: string; -} - -export interface LowCodeComponentType { - /** - * 研发模式 - */ - devMode: 'lowCode'; - /** - * 组件名称 - */ - componentName: string; -} - -export type ProCodeComponentType = NpmInfo; -export type ComponentMap = ProCodeComponentType | LowCodeComponentType; - -export function isProCodeComponentType(desc: ComponentMap): desc is ProCodeComponentType { - return 'package' in desc; -} - -export function isLowCodeComponentType(desc: ComponentMap): desc is LowCodeComponentType { - return !isProCodeComponentType(desc); -} - -export type ComponentsMap = ComponentMap[]; diff --git a/packages/types/src/prop-config.ts b/packages/types/src/prop-config.ts deleted file mode 100644 index 502a23ce7c..0000000000 --- a/packages/types/src/prop-config.ts +++ /dev/null @@ -1,65 +0,0 @@ -export type PropType = BasicType | RequiredType | ComplexType; -export type BasicType = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any'; -export type ComplexType = OneOf | OneOfType | ArrayOf | ObjectOf | Shape | Exact; - -export interface RequiredType { - type: BasicType; - isRequired?: boolean; -} - -export interface OneOf { - type: 'oneOf'; - value: string[]; - isRequired?: boolean; -} -export interface OneOfType { - type: 'oneOfType'; - value: PropType[]; - isRequired?: boolean; -} -export interface ArrayOf { - type: 'arrayOf'; - value: PropType; - isRequired?: boolean; -} -export interface ObjectOf { - type: 'objectOf'; - value: PropType; - isRequired?: boolean; -} -export interface Shape { - type: 'shape'; - value: PropConfig[]; - isRequired?: boolean; -} -export interface Exact { - type: 'exact'; - value: PropConfig[]; - isRequired?: boolean; -} - -/** - * 组件属性信息 - */ -export interface PropConfig { - /** - * 属性名称 - */ - name: string; - /** - * 属性类型 - */ - propType: PropType; - /** - * 属性描述 - */ - description?: string; - /** - * 属性默认值 - */ - defaultValue?: any; - /** - * @deprecated 已被弃用 - */ - setter?: any; -} diff --git a/packages/types/src/schema.ts b/packages/types/src/schema.ts deleted file mode 100644 index 5d0fad4444..0000000000 --- a/packages/types/src/schema.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { InterpretDataSource as DataSource } from '@alilc/lowcode-datasource-types'; -import { ComponentsMap } from './npm'; -import { - CompositeValue, - JSExpression, - JSFunction, - CompositeObject, - JSONObject, -} from './value-type'; -import { I18nMap } from './i18n'; -import { UtilsMap } from './utils'; -import { AppConfig } from './app-config'; - -// 转换成一个 .jsx 文件内 React Class 类 render 函数返回的 jsx 代码 - -/** - * 搭建基础协议 - 单个组件树节点描述 - */ -export interface NodeSchema { - id?: string; - /** - * 组件名称 必填、首字母大写 - */ - componentName: string; - /** - * 组件属性对象 - */ - props?: { - children?: NodeData | NodeData[]; - } & PropsMap;// | PropsList; - /** - * 组件属性对象 - */ - leadingComponents?: string; - /** - * 渲染条件 - */ - condition?: CompositeValue; - /** - * 循环数据 - */ - loop?: CompositeValue; - /** - * 循环迭代对象、索引名称 ["item", "index"] - */ - loopArgs?: [string, string]; - /** - * 子节点 - */ - children?: NodeData | NodeData[]; - /** - * 是否锁定 - */ - isLocked?: boolean; - - // @todo - // ------- future support ----- - conditionGroup?: string; - title?: string; - ignore?: boolean; - locked?: boolean; - hidden?: boolean; - isTopFixed?: boolean; - - /** @experimental 编辑态内部使用 */ - __ctx?: any; -} - -export type PropsMap = CompositeObject; -export type PropsList = Array<{ - spread?: boolean; - name?: string; - value: CompositeValue; -}>; - -export type NodeData = NodeSchema | JSExpression | DOMText; -export type NodeDataType = NodeData | NodeData[]; - -export function isDOMText(data: any): data is DOMText { - return typeof data === 'string'; -} - -export type DOMText = string; - -/** - * 容器结构描述 - */ -export interface ContainerSchema extends NodeSchema { - /** - * 'Block' | 'Page' | 'Component'; - */ - componentName: string; - /** - * 文件名称 - */ - fileName: string; - /** - * @todo 待文档定义 - */ - meta?: Record<string, unknown>; - /** - * 容器初始数据 - */ - state?: { - [key: string]: CompositeValue; - }; - /** - * 自定义方法设置 - */ - methods?: { - [key: string]: JSExpression | JSFunction; - }; - /** - * 生命周期对象 - */ - lifeCycles?: { - // @todo 生命周期对象建议改为闭合集合 - [key: string]: JSExpression | JSFunction; - }; - /** - * 样式文件 - */ - css?: string; - /** - * 异步数据源配置 - */ - dataSource?: DataSource; - /** - * 低代码业务组件默认属性 - */ - defaultProps?: CompositeObject; - // @todo propDefinitions -} - -/** - * 页面容器 - * @see https://lowcode-engine.cn/lowcode - */ -export interface PageSchema extends ContainerSchema { - componentName: 'Page'; -} - -/** - * 低代码业务组件容器 - * @see https://lowcode-engine.cn/lowcode - */ -export interface ComponentSchema extends ContainerSchema { - componentName: 'Component'; -} - -/** - * 区块容器 - * @see https://lowcode-engine.cn/lowcode - */ -export interface BlockSchema extends ContainerSchema { - componentName: 'Block'; -} - -/** - * @todo - */ -export type RootSchema = PageSchema | ComponentSchema | BlockSchema; - -/** - * Slot schema 描述 - */ -export interface SlotSchema extends NodeSchema { - componentName: 'Slot'; - name?: string; - params?: string[]; -} - -/** - * 应用描述 - */ -export interface ProjectSchema { - id?: string; - /** - * 当前应用协议版本号 - */ - version: string; - /** - * 当前应用所有组件映射关系 - */ - componentsMap: ComponentsMap; - /** - * 描述应用所有页面、低代码组件的组件树 - * 低代码业务组件树描述 - * 是长度固定为1的数组, 即数组内仅包含根容器的描述(低代码业务组件容器类型) - */ - componentsTree: RootSchema[]; - /** - * 国际化语料 - */ - i18n?: I18nMap; - /** - * 应用范围内的全局自定义函数或第三方工具类扩展 - */ - utils?: UtilsMap; - /** - * 应用范围内的全局常量 - */ - constants?: JSONObject; - /** - * 应用范围内的全局样式 - */ - css?: string; - /** - * 当前应用的公共数据源 - */ - dataSource?: DataSource; - /** - * 当前应用配置信息 - */ - config?: AppConfig | Record<string, any>; - /** - * 当前应用元数据信息 - */ - meta?: Record<string, any>; -} - -export function isNodeSchema(data: any): data is NodeSchema { - return data && data.componentName; -} - -export function isProjectSchema(data: any): data is ProjectSchema { - return data && data.componentsTree; -} diff --git a/packages/types/src/setter-config.ts b/packages/types/src/setter-config.ts deleted file mode 100644 index d579240eff..0000000000 --- a/packages/types/src/setter-config.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { ComponentClass, Component, ComponentType, ReactElement, isValidElement } from 'react'; -import { TitleContent } from './title'; -import { SettingTarget } from './setting-target'; -import { CompositeValue } from './value-type'; - -function isReactClass(obj: any): obj is ComponentClass<any> { - return obj && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component); -} - -function isReactComponent(obj: any): obj is ComponentType<any> { - return obj && (isReactClass(obj) || typeof obj === 'function'); -} - -export type CustomView = ReactElement | ComponentType<any>; - -export type DynamicProps = (target: SettingTarget) => Record<string, unknown>; -export type DynamicSetter = (target: SettingTarget) => string | SetterConfig | CustomView; - -/** - * 设置器配置 - */ -export interface SetterConfig { - // if *string* passed must be a registered Setter Name - /** - * 配置设置器用哪一个 setter - */ - componentName: string | CustomView; - /** - * 传递给 setter 的属性 - * - * the props pass to Setter Component - */ - props?: Record<string, unknown> | DynamicProps; - /** - * @deprecated - */ - children?: any; - /** - * 是否必填? - * - * ArraySetter 里有个快捷预览,可以在不打开面板的情况下直接编辑 - */ - isRequired?: boolean; - /** - * Setter 的初始值 - * - * @todo initialValue 可能要和 defaultValue 二选一 - */ - initialValue?: any | ((target: SettingTarget) => any); - // for MixedSetter - /** - * 给 MixedSetter 时切换 Setter 展示用的 - */ - title?: TitleContent; - // for MixedSetter check this is available - /** - * 给 MixedSetter 用于判断优先选中哪个 - */ - condition?: (target: SettingTarget) => boolean; - /** - * 给 MixedSetter,切换值时声明类型 - * - * @todo 物料协议推进 - */ - valueType?: CompositeValue[]; - // 标识是否为动态setter,默认为true - isDynamic?: boolean; -} - -// if *string* passed must be a registered Setter Name, future support blockSchema -export type SetterType = SetterConfig | SetterConfig[] | string | CustomView; - -export function isSetterConfig(obj: any): obj is SetterConfig { - return obj && typeof obj === 'object' && 'componentName' in obj && !isCustomView(obj); -} - -export function isCustomView(obj: any): obj is CustomView { - return obj && (isValidElement(obj) || isReactComponent(obj)); -} - -export function isDynamicSetter(obj: any): obj is DynamicSetter { - return obj && typeof obj === 'function' && !isReactClass(obj); -} diff --git a/packages/types/src/setting-target.ts b/packages/types/src/setting-target.ts deleted file mode 100644 index 9d2a3b1fae..0000000000 --- a/packages/types/src/setting-target.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { IEditor } from './editor'; - -export interface SettingTarget { - /** - * 同样类型的节点 - */ - readonly isSameComponent: boolean; - - /** - * 一个 - */ - readonly isSingle: boolean; - - /** - * 多个 - */ - readonly isMultiple: boolean; - - /** - * 编辑器引用 - */ - readonly editor: IEditor; - - /** - * 访问路径 - */ - readonly path: Array<string| number>; - - /** - * 顶端 - */ - readonly top: SettingTarget; - - /** - * 父级 - */ - readonly parent: SettingTarget; - - - /** - * 获取当前值 - */ - getValue: () => any; - - /** - * 设置当前值 - */ - setValue: (value: any) => void; - - /** - * 取得子项 - */ - get: (propName: string | number) => SettingTarget | null; - - /** - * 取得子项 - */ - getProps?: () => SettingTarget; - - /** - * 获取子项属性值 - */ - getPropValue: (propName: string | number) => any; - - /** - * 设置子项属性值 - */ - setPropValue: (propName: string | number, value: any) => void; - - /** - * 清除已设置值 - */ - clearPropValue: (propName: string | number) => void; - - /** - * 获取顶层附属属性值 - */ - getExtraPropValue: (propName: string) => any; - - /** - * 设置顶层附属属性值 - */ - setExtraPropValue: (propName: string, value: any) => void; - - // @todo 补充 node 定义 - /** - * 获取 node 中的第一项 - */ - getNode: () => any; -} diff --git a/packages/types/src/shell-model-factory.ts b/packages/types/src/shell-model-factory.ts new file mode 100644 index 0000000000..b6044ced9e --- /dev/null +++ b/packages/types/src/shell-model-factory.ts @@ -0,0 +1,9 @@ +import { IPublicModelNode, IPublicModelSettingField } from './shell'; + +export interface IShellModelFactory { + // TODO: 需要给 innerNode 提供一个 interface 并用在这里 + createNode(node: any | null | undefined): IPublicModelNode | null; + // TODO: 需要给 InnerSettingField 提供一个 interface 并用在这里 + + createSettingField(prop: any): IPublicModelSettingField; +} diff --git a/packages/types/src/shell/api/canvas.ts b/packages/types/src/shell/api/canvas.ts new file mode 100644 index 0000000000..6cb3df9fc5 --- /dev/null +++ b/packages/types/src/shell/api/canvas.ts @@ -0,0 +1,73 @@ +import { IPublicModelDragon, IPublicModelDropLocation, IPublicModelScrollTarget, IPublicModelScroller, IPublicModelActiveTracker, IPublicModelClipboard } from '../model'; +import { IPublicTypeLocationData, IPublicTypeScrollable } from '../type'; + +/** + * canvas - 画布 API + * @since v1.1.0 + */ +export interface IPublicApiCanvas { + + /** + * 创一个滚动控制器 Scroller,赋予一个视图滚动的基本能力, + * + * a Scroller is a controller that gives a view (IPublicTypeScrollable) the ability scrolling + * to some cordination by api scrollTo. + * + * when a scroller is inited, will need to pass is a scrollable, which has a scrollTarget. + * and when scrollTo(options: { left?: number; top?: number }) is called, scroller will + * move scrollTarget`s top-left corner to (options.left, options.top) that passed in. + * @since v1.1.0 + */ + createScroller(scrollable: IPublicTypeScrollable): IPublicModelScroller; + + /** + * 创建一个 ScrollTarget,与 Scroller 一起发挥作用,详见 createScroller 中的描述 + * + * this works with Scroller, refer to createScroller`s description + * @since v1.1.0 + */ + createScrollTarget(shell: HTMLDivElement): IPublicModelScrollTarget; + + /** + * 创建一个文档插入位置对象,该对象用来描述一个即将插入的节点在文档中的位置 + * + * create a drop location for document, drop location describes a location in document + * @since v1.1.0 + */ + createLocation(locationData: IPublicTypeLocationData): IPublicModelDropLocation; + + /** + * 获取拖拽操作对象的实例 + * + * get dragon instance, you can use this to obtain draging related abilities and lifecycle hooks + * @since v1.1.0 + */ + get dragon(): IPublicModelDragon | null; + + /** + * 获取活动追踪器实例 + * + * get activeTracker instance, which is a singleton running in engine. + * it tracks document`s current focusing node/node[], and notify it`s subscribers that when + * focusing node/node[] changed. + * @since v1.1.0 + */ + get activeTracker(): IPublicModelActiveTracker | null; + + /** + * 是否处于 LiveEditing 状态 + * + * check if canvas is in liveEditing state + * @since v1.1.0 + */ + get isInLiveEditing(): boolean; + + /** + * 获取全局剪贴板实例 + * + * get clipboard instance + * + * @since v1.1.0 + */ + get clipboard(): IPublicModelClipboard; +} 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/common.ts b/packages/types/src/shell/api/common.ts new file mode 100644 index 0000000000..05ef0da17f --- /dev/null +++ b/packages/types/src/shell/api/common.ts @@ -0,0 +1,124 @@ + +import { Component, ReactNode } from 'react'; +import { IPublicTypeI18nData, IPublicTypeNodeSchema, IPublicTypeTitleContent } from '../type'; +import { IPublicEnumTransitionType } from '../enum'; + +export interface IPublicApiCommonUtils { + + /** + * 是否为合法的 schema 结构 + * check if data is valid NodeSchema + * + * @param {*} data + * @returns {boolean} + */ + isNodeSchema(data: any): boolean; + + /** + * 是否为表单事件类型 + * check if e is a form event + * @param {(KeyboardEvent | MouseEvent)} e + * @returns {boolean} + */ + isFormEvent(e: KeyboardEvent | MouseEvent): boolean; + + /** + * 从 schema 结构中查找指定 id 节点 + * get node schema from a larger schema with node id + * @param {IPublicTypeNodeSchema} schema + * @param {string} nodeId + * @returns {(IPublicTypeNodeSchema | undefined)} + */ + getNodeSchemaById( + schema: IPublicTypeNodeSchema, + nodeId: string, + ): IPublicTypeNodeSchema | undefined; + + // TODO: add comments + getConvertedExtraKey(key: string): string; + + // TODO: add comments + getOriginalExtraKey(key: string): string; + + /** + * 批处理事务,用于优化特定场景的性能 + * excute something in a transaction for performence + * + * @param {() => void} fn + * @param {IPublicEnumTransitionType} type + * @since v1.0.16 + */ + executeTransaction(fn: () => void, type: IPublicEnumTransitionType): void; + + /** + * i18n 相关工具 + * i18n tools + * + * @param {(string | object)} instance + * @returns {{ + * intlNode(id: string, params?: object): ReactNode; + * intl(id: string, params?: object): string; + * getLocale(): string; + * setLocale(locale: string): void; + * }} + * @since v1.0.17 + */ + createIntl(instance: string | object): { + intlNode(id: string, params?: object): ReactNode; + intl(id: string, params?: object): string; + getLocale(): string; + setLocale(locale: string): void; + }; + + /** + * i18n 转换方法 + */ + intl(data: IPublicTypeI18nData | string, params?: object): string; +} +export interface IPublicApiCommonSkeletonCabin { + + /** + * 编辑器框架 View + * get Workbench Component + */ + get Workbench(): Component; +} + +export interface IPublicApiCommonEditorCabin { + + /** + * Title 组件 + * @experimental unstable API, pay extra caution when trying to use this + */ + get Tip(): React.ComponentClass<{}>; + + /** + * Tip 组件 + * @experimental unstable API, pay extra caution when trying to use this + */ + get Title(): React.ComponentClass<{ + title: IPublicTypeTitleContent | undefined; + match?: boolean; + keywords?: string | null; + }>; +} + +export interface IPublicApiCommonDesignerCabin { +} + +export interface IPublicApiCommon { + + get utils(): IPublicApiCommonUtils; + + /** + * @deprecated + */ + get designerCabin(): IPublicApiCommonDesignerCabin; + + /** + * @experimental unstable API, pay extra caution when trying to use this + */ + get editorCabin(): IPublicApiCommonEditorCabin; + + get skeletonCabin(): IPublicApiCommonSkeletonCabin; +} diff --git a/packages/types/src/shell/api/commonUI.ts b/packages/types/src/shell/api/commonUI.ts new file mode 100644 index 0000000000..5ac025fcde --- /dev/null +++ b/packages/types/src/shell/api/commonUI.ts @@ -0,0 +1,74 @@ +import React, { ReactElement } from 'react'; +import { IPublicTypeContextMenuAction, IPublicTypeHelpTipConfig, IPublicTypeTipConfig, IPublicTypeTitleContent } from '../type'; +import { Balloon, Breadcrumb, Button, Card, Checkbox, DatePicker, Dialog, Dropdown, Form, Icon, Input, Loading, Message, Overlay, Pagination, Radio, Search, Select, SplitButton, Step, Switch, Tab, Table, Tree, TreeSelect, Upload, Divider } from '@alifd/next'; +import { IconProps } from '@alifd/next/types/icon'; + +export interface IPublicApiCommonUI { + Balloon: typeof Balloon; + Breadcrumb: typeof Breadcrumb; + Button: typeof Button; + Card: typeof Card; + Checkbox: typeof Checkbox; + DatePicker: typeof DatePicker; + Dialog: typeof Dialog; + Dropdown: typeof Dropdown; + Form: typeof Form; + Icon: typeof Icon; + Input: typeof Input; + Loading: typeof Loading; + Message: typeof Message; + Overlay: typeof Overlay; + Pagination: typeof Pagination; + Radio: typeof Radio; + Search: typeof Search; + Select: typeof Select; + SplitButton: typeof SplitButton; + Step: typeof Step; + Switch: typeof Switch; + Tab: typeof Tab; + Table: typeof Table; + Tree: typeof Tree; + TreeSelect: typeof TreeSelect; + Upload: typeof Upload; + Divider: typeof Divider; + + /** + * Title 组件 + */ + get Tip(): React.ComponentClass<IPublicTypeTipConfig>; + + /** + * HelpTip 组件 + */ + get HelpTip(): React.VFC<{ + help: IPublicTypeHelpTipConfig; + + /** + * 方向 + * @default 'top' + */ + direction: IPublicTypeTipConfig['direction']; + + /** + * 大小 + * @default 'small' + */ + size: IconProps['size']; + }>; + + /** + * Tip 组件 + */ + get Title(): React.ComponentClass<{ + title: IPublicTypeTitleContent | undefined; + match?: boolean; + keywords?: string | null; + }>; + + get ContextMenu(): ((props: { + menus: IPublicTypeContextMenuAction[]; + children: React.ReactElement[] | React.ReactElement; + }) => ReactElement) & { + create(menus: IPublicTypeContextMenuAction[], event: MouseEvent | React.MouseEvent): void; + }; +} diff --git a/packages/types/src/shell/api/event.ts b/packages/types/src/shell/api/event.ts new file mode 100644 index 0000000000..5b8c59e139 --- /dev/null +++ b/packages/types/src/shell/api/event.ts @@ -0,0 +1,37 @@ +import { IPublicTypeDisposable } from '../type'; + +export interface IPublicApiEvent { + + /** + * 监听事件 + * add monitor to a event + * @param event 事件名称 + * @param listener 事件回调 + */ + on(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable; + + /** + * 监听事件,会在其他回调函数之前执行 + * add monitor to a event + * @param event 事件名称 + * @param listener 事件回调 + */ + prependListener(event: string, listener: (...args: any[]) => void): IPublicTypeDisposable; + + /** + * 取消监听事件 + * cancel a monitor from a event + * @param event 事件名称 + * @param listener 事件回调 + */ + off(event: string, listener: (...args: any[]) => void): void; + + /** + * 触发事件 + * emit a message for a event + * @param event 事件名称 + * @param args 事件参数 + * @returns + */ + emit(event: string, ...args: any[]): void; +} diff --git a/packages/types/src/shell/api/hotkey.ts b/packages/types/src/shell/api/hotkey.ts new file mode 100644 index 0000000000..894eb0e2f9 --- /dev/null +++ b/packages/types/src/shell/api/hotkey.ts @@ -0,0 +1,25 @@ +import { IPublicTypeDisposable, IPublicTypeHotkeyCallback, IPublicTypeHotkeyCallbacks } from '../type'; + +export interface IPublicApiHotkey { + + /** + * 获取当前快捷键配置 + * + * @experimental + * @since v1.1.0 + */ + get callbacks(): IPublicTypeHotkeyCallbacks; + + /** + * 绑定快捷键 + * bind hotkey/hotkeys, + * @param combos 快捷键,格式如:['command + s'] 、['ctrl + shift + s'] 等 + * @param callback 回调函数 + * @param action + */ + bind( + combos: string[] | string, + callback: IPublicTypeHotkeyCallback, + action?: string, + ): IPublicTypeDisposable; +} diff --git a/packages/types/src/shell/api/index.ts b/packages/types/src/shell/api/index.ts new file mode 100644 index 0000000000..8f14d8dadd --- /dev/null +++ b/packages/types/src/shell/api/index.ts @@ -0,0 +1,14 @@ +export * from './common'; +export * from './event'; +export * from './hotkey'; +export * from './material'; +export * from './project'; +export * from './setters'; +export * from './simulator-host'; +export * from './skeleton'; +export * from './plugins'; +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/logger.ts b/packages/types/src/shell/api/logger.ts new file mode 100644 index 0000000000..db81aeaaed --- /dev/null +++ b/packages/types/src/shell/api/logger.ts @@ -0,0 +1,33 @@ +export type LoggerLevel = 'debug' | 'log' | 'info' | 'warn' | 'error'; +export interface ILoggerOptions { + level?: LoggerLevel; + bizName?: string; +} + +export interface IPublicApiLogger { + + /** + * debug info + */ + debug(...args: any | any[]): void; + + /** + * normal info output + */ + info(...args: any | any[]): void; + + /** + * warning info output + */ + warn(...args: any | any[]): void; + + /** + * error info output + */ + error(...args: any | any[]): void; + + /** + * log info output + */ + log(...args: any | any[]): void; +} diff --git a/packages/types/src/shell/api/material.ts b/packages/types/src/shell/api/material.ts new file mode 100644 index 0000000000..89b2b39ad1 --- /dev/null +++ b/packages/types/src/shell/api/material.ts @@ -0,0 +1,149 @@ +import { IPublicTypeAssetsJson, IPublicTypeMetadataTransducer, IPublicTypeComponentAction, IPublicTypeNpmInfo, IPublicTypeDisposable, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '../type'; +import { IPublicModelComponentMeta } from '../model'; +import { ComponentType } from 'react'; + +export interface IPublicApiMaterial { + + /** + * 获取组件 map 结构 + * get map of components + */ + get componentsMap(): { [key: string]: IPublicTypeNpmInfo | ComponentType<any> | object } ; + + /** + * 设置「资产包」结构 + * set data for Assets + * @returns void + */ + setAssets(assets: IPublicTypeAssetsJson): Promise<void>; + + /** + * 获取「资产包」结构 + * get AssetsJson data + * @returns IPublicTypeAssetsJson + */ + getAssets(): IPublicTypeAssetsJson | undefined; + + /** + * 加载增量的「资产包」结构,该增量包会与原有的合并 + * load Assets incrementally, and will merge this with exiting assets + * @param incrementalAssets + * @returns + */ + loadIncrementalAssets(incrementalAssets: IPublicTypeAssetsJson): void; + + /** + * 注册物料元数据管道函数,在物料信息初始化时执行。 + * register transducer to process component meta, which will be + * excuted during component meta`s initialization + * @param transducer + * @param level + * @param id + */ + registerMetadataTransducer( + transducer: IPublicTypeMetadataTransducer, + level?: number, + id?: string | undefined + ): void; + + /** + * 获取所有物料元数据管道函数 + * get all registered metadata transducers + * @returns {IPublicTypeMetadataTransducer[]} + */ + getRegisteredMetadataTransducers(): IPublicTypeMetadataTransducer[]; + + /** + * 获取指定名称的物料元数据 + * get component meta by component name + * @param componentName + * @returns + */ + getComponentMeta(componentName: string): IPublicModelComponentMeta | null; + + /** + * test if the given object is a ComponentMeta instance or not + * @param obj + * @experiemental unstable API, pay extra caution when trying to use it + */ + isComponentMeta(obj: any): boolean; + + /** + * 获取所有已注册的物料元数据 + * get map of all component metas + */ + getComponentMetasMap(): Map<string, IPublicModelComponentMeta>; + + /** + * 在设计器辅助层增加一个扩展 action + * + * add an action button in canvas context menu area + * @param action + * @example + * ```ts + * import { plugins } from '@alilc/lowcode-engine'; + * import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + * + * const removeCopyAction = (ctx: IPublicModelPluginContext) => { + * return { + * async init() { + * const { removeBuiltinComponentAction } = ctx.material; + * removeBuiltinComponentAction('copy'); + * } + * } + * }; + * removeCopyAction.pluginName = 'removeCopyAction'; + * await plugins.register(removeCopyAction); + * ``` + */ + addBuiltinComponentAction(action: IPublicTypeComponentAction): void; + + /** + * 移除设计器辅助层的指定 action + * remove a builtin action button from canvas context menu area + * @param name + */ + removeBuiltinComponentAction(name: string): void; + + /** + * 修改已有的设计器辅助层的指定 action + * modify a builtin action button in canvas context menu area + * @param actionName + * @param handle + */ + modifyBuiltinComponentAction( + actionName: string, + handle: (action: IPublicTypeComponentAction) => void, + ): void; + + /** + * 监听 assets 变化的事件 + * add callback for assets changed event + * @param fn + */ + onChangeAssets(fn: () => void): IPublicTypeDisposable; + + /** + * 刷新 componentMetasMap,可触发模拟器里的 components 重新构建 + * @since v1.1.7 + */ + refreshComponentMetasMap(): void; + + /** + * 添加右键菜单项 + * @param action + */ + addContextMenuOption(action: IPublicTypeContextMenuAction): void; + + /** + * 删除特定右键菜单项 + * @param name + */ + removeContextMenuOption(name: string): void; + + /** + * 调整右键菜单项布局 + * @param actions + */ + adjustContextMenuLayout(fn: (actions: IPublicTypeContextMenuItem[]) => IPublicTypeContextMenuItem[]): void; +} diff --git a/packages/types/src/shell/api/plugins.ts b/packages/types/src/shell/api/plugins.ts new file mode 100644 index 0000000000..a930162909 --- /dev/null +++ b/packages/types/src/shell/api/plugins.ts @@ -0,0 +1,63 @@ +import { IPublicModelPluginInstance, IPublicTypePlugin } from '../model'; +import { IPublicTypePreferenceValueType } from '../type'; +import { IPublicTypePluginRegisterOptions } from '../type/plugin-register-options'; + +export interface IPluginPreferenceMananger { + // eslint-disable-next-line max-len + getPreferenceValue: ( + key: string, + defaultValue?: IPublicTypePreferenceValueType, + ) => IPublicTypePreferenceValueType | undefined; +} + +export type PluginOptionsType = string | number | boolean | object; + +export interface IPublicApiPlugins { + /** + * 可以通过 plugin api 获取其他插件 export 导出的内容 + */ + [key: string]: any; + + register( + pluginModel: IPublicTypePlugin, + options?: Record<string, PluginOptionsType>, + registerOptions?: IPublicTypePluginRegisterOptions, + ): Promise<void>; + + /** + * 引擎初始化时可以提供全局配置给到各插件,通过这个方法可以获得本插件对应的配置 + * + * use this to get preference config for this plugin when engine.init() called + */ + getPluginPreference( + pluginName: string, + ): Record<string, IPublicTypePreferenceValueType> | null | undefined; + + /** + * 获取指定插件 + * + * get plugin instance by name + */ + get(pluginName: string): IPublicModelPluginInstance | null; + + /** + * 获取所有的插件实例 + * + * get all plugin instances + */ + getAll(): IPublicModelPluginInstance[]; + + /** + * 判断是否有指定插件 + * + * check if plugin with certain name exists + */ + has(pluginName: string): boolean; + + /** + * 删除指定插件 + * + * delete plugin instance by name + */ + delete(pluginName: string): void; +} diff --git a/packages/types/src/shell/api/project.ts b/packages/types/src/shell/api/project.ts new file mode 100644 index 0000000000..662f302ccc --- /dev/null +++ b/packages/types/src/shell/api/project.ts @@ -0,0 +1,147 @@ +import { IPublicTypeProjectSchema, IPublicTypeDisposable, IPublicTypeRootSchema, IPublicTypePropsTransducer, IPublicTypeAppConfig } from '../type'; +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicApiSimulatorHost } from './'; +import { IPublicModelDocumentModel } from '../model'; + +export interface IBaseApiProject< + DocumentModel +> { + + /** + * 获取当前的 document + * get current document + */ + get currentDocument(): DocumentModel | null; + + /** + * 获取当前 project 下所有 documents + * get all documents of this project + * @returns + */ + get documents(): DocumentModel[]; + + /** + * 获取模拟器的 host + * get simulator host + */ + get simulatorHost(): IPublicApiSimulatorHost | null; + + /** + * 打开一个 document + * open a document + * @param doc + * @returns + */ + openDocument(doc?: string | IPublicTypeRootSchema | undefined): DocumentModel | null; + + /** + * 创建一个 document + * create a document + * @param data + * @returns + */ + createDocument(data?: IPublicTypeRootSchema): DocumentModel | null; + + /** + * 删除一个 document + * remove a document + * @param doc + */ + removeDocument(doc: DocumentModel): void; + + /** + * 根据 fileName 获取 document + * get a document by filename + * @param fileName + * @returns + */ + getDocumentByFileName(fileName: string): DocumentModel | null; + + /** + * 根据 id 获取 document + * get a document by id + * @param id + * @returns + */ + getDocumentById(id: string): DocumentModel | null; + + /** + * 导出 project + * export project to schema + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage): IPublicTypeProjectSchema; + + /** + * 导入 project schema + * import schema to project + * @param schema 待导入的 project 数据 + */ + importSchema(schema?: IPublicTypeProjectSchema): void; + + /** + * 获取当前的 document + * get current document + * @returns + */ + getCurrentDocument(): DocumentModel | null; + + /** + * 增加一个属性的管道处理函数 + * add a transducer to process prop + * @param transducer + * @param stage + */ + addPropsTransducer( + transducer: IPublicTypePropsTransducer, + stage: IPublicEnumTransformStage, + ): void; + + /** + * 绑定删除文档事件 + * set callback for event onDocumentRemoved + * @param fn + * @since v1.0.16 + */ + onRemoveDocument(fn: (data: { id: string }) => void): IPublicTypeDisposable; + + /** + * 当前 project 内的 document 变更事件 + * set callback for event onDocumentChanged + */ + onChangeDocument(fn: (doc: DocumentModel) => void): IPublicTypeDisposable; + + /** + * 当前 project 的模拟器 ready 事件 + * set callback for event onSimulatorHostReady + */ + onSimulatorHostReady(fn: (host: IPublicApiSimulatorHost) => void): IPublicTypeDisposable; + + /** + * 当前 project 的渲染器 ready 事件 + * set callback for event onSimulatorRendererReady + */ + onSimulatorRendererReady(fn: () => void): IPublicTypeDisposable; + + /** + * 设置多语言语料 + * 数据格式参考 https://github.com/alibaba/lowcode-engine/blob/main/specs/lowcode-spec.md#2434%E5%9B%BD%E9%99%85%E5%8C%96%E5%A4%9A%E8%AF%AD%E8%A8%80%E7%B1%BB%E5%9E%8Baa + * + * set I18n data for this project + * @param value object + * @since v1.0.17 + */ + setI18n(value: object): void; + + /** + * 设置当前项目配置 + * + * set config data for this project + * @param value object + * @since v1.1.4 + */ + setConfig<T extends keyof IPublicTypeAppConfig>(key: T, value: IPublicTypeAppConfig[T]): void; + setConfig(value: IPublicTypeAppConfig): void; +} + +export interface IPublicApiProject extends IBaseApiProject<IPublicModelDocumentModel> {} diff --git a/packages/types/src/shell/api/setters.ts b/packages/types/src/shell/api/setters.ts new file mode 100644 index 0000000000..011a9dcacd --- /dev/null +++ b/packages/types/src/shell/api/setters.ts @@ -0,0 +1,40 @@ +import { ReactNode } from 'react'; + +import { IPublicTypeRegisteredSetter, IPublicTypeCustomView } from '../type'; + +export interface IPublicApiSetters { + + /** + * 获取指定 setter + * get setter by type + * @param type + * @returns + */ + getSetter(type: string): IPublicTypeRegisteredSetter | null; + + /** + * 获取已注册的所有 settersMap + * get map of all registered setters + * @returns + */ + getSettersMap(): Map<string, IPublicTypeRegisteredSetter & { + type: string; + }>; + + /** + * 注册一个 setter + * register a setter + * @param typeOrMaps + * @param setter + * @returns + */ + registerSetter( + typeOrMaps: string | { [key: string]: IPublicTypeCustomView | IPublicTypeRegisteredSetter }, + setter?: IPublicTypeCustomView | IPublicTypeRegisteredSetter | undefined + ): void; + + /** + * @deprecated + */ + createSetterContent (setter: any, props: Record<string, any>): ReactNode; +} diff --git a/packages/types/src/shell/api/simulator-host.ts b/packages/types/src/shell/api/simulator-host.ts new file mode 100644 index 0000000000..9137067951 --- /dev/null +++ b/packages/types/src/shell/api/simulator-host.ts @@ -0,0 +1,51 @@ +import { IPublicModelNode, IPublicModelSimulatorRender } from '../model'; + +export interface IPublicApiSimulatorHost { + + /** + * 获取 contentWindow + * @experimental unstable api, pay extra caution when trying to use it + */ + get contentWindow(): Window | undefined; + + /** + * 获取 contentDocument + * @experimental unstable api, pay extra caution when trying to use it + */ + get contentDocument(): Document | undefined; + + /** + * @experimental unstable api, pay extra caution when trying to use it + */ + get renderer(): IPublicModelSimulatorRender | undefined; + + /** + * 设置若干用于画布渲染的变量,比如画布大小、locale 等。 + * set config for simulator host, eg. device locale and so on. + * @param key + * @param value + */ + set(key: string, value: any): void; + + /** + * 获取模拟器中设置的变量,比如画布大小、locale 等。 + * set config value by key + * @param key + * @returns + */ + get(key: string): any; + + /** + * 滚动到指定节点 + * scroll to specific node + * @param node + * @since v1.1.0 + */ + scrollToNode(node: IPublicModelNode): void; + + /** + * 刷新渲染画布 + * make simulator render again + */ + rerender(): void; +} diff --git a/packages/types/src/shell/api/skeleton.ts b/packages/types/src/shell/api/skeleton.ts new file mode 100644 index 0000000000..1bf788e121 --- /dev/null +++ b/packages/types/src/shell/api/skeleton.ts @@ -0,0 +1,152 @@ +import { IPublicModelSkeletonItem } from '../model'; +import { IPublicTypeConfigTransducer, IPublicTypeDisposable, IPublicTypeSkeletonConfig, IPublicTypeWidgetConfigArea } from '../type'; + +export interface IPublicApiSkeleton { + + /** + * 增加一个面板实例 + * add a new panel + * @param config + * @param extraConfig + * @returns + */ + add(config: IPublicTypeSkeletonConfig, extraConfig?: Record<string, any>): IPublicModelSkeletonItem | undefined; + + /** + * 移除一个面板实例 + * remove a panel + * @param config + * @returns + */ + remove(config: IPublicTypeSkeletonConfig): number | undefined; + + /** + * 获取某个区域下的所有面板实例 + * @param areaName IPublicTypeWidgetConfigArea + */ + getAreaItems(areaName: IPublicTypeWidgetConfigArea): IPublicModelSkeletonItem[] | undefined; + + /** + * 获取面板实例 + * @param name 面板名称 + * @since v1.1.10 + */ + getPanel(name: string): IPublicModelSkeletonItem | undefined; + + /** + * 展示指定 Panel 实例 + * show panel by name + * @param name + */ + showPanel(name: string): void; + + /** + * 隐藏面板 + * hide panel by name + * @param name + */ + hidePanel(name: string): void; + + /** + * 展示指定 Widget 实例 + * show widget by name + * @param name + */ + showWidget(name: string): void; + + /** + * 将 widget 启用 + * enable widget by name + * @param name + */ + enableWidget(name: string): void; + + /** + * 隐藏指定 widget 实例 + * hide widget by name + * @param name + */ + hideWidget(name: string): void; + + /** + * 将 widget 禁用掉,禁用后,所有鼠标事件都会被禁止掉。 + * disable widget,and make it not responding any click event. + * @param name + */ + disableWidget(name: string): void; + + /** + * 显示某个 Area + * show area + * @param areaName name of area + */ + showArea(areaName: string): void; + + /** + * 隐藏某个 Area + * hide area + * @param areaName name of area + */ + hideArea(areaName: string): void; + + /** + * 监听 Panel 实例显示事件 + * set callback for panel shown event + * @param listener + * @returns + */ + onShowPanel(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 监听 Panel 实例隐藏事件 + * set callback for panel hidden event + * @param listener + * @returns + */ + onHidePanel(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 监听 Widget 实例 Disable 事件 + * @param listener + */ + onDisableWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 监听 Widget 实例 Enable 事件 + * @param listener + */ + onEnableWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 监听 Widget 显示事件 + * set callback for widget shown event + * @param listener + * @returns + */ + onShowWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 监听 Widget 隐藏事件 + * set callback for widget hidden event + * @param listener + * @returns + */ + onHideWidget(listener: (paneName?: string, panel?: IPublicModelSkeletonItem) => void): IPublicTypeDisposable; + + /** + * 注册一个面板的配置转换器(transducer)。 + * Registers a configuration transducer for a panel. + * @param {IPublicTypeConfigTransducer} transducer + * - 要注册的转换器函数。该函数接受一个配置对象(类型为 IPublicTypeSkeletonConfig)作为输入,并返回修改后的配置对象。 + * - The transducer function to be registered. This function takes a configuration object (of type IPublicTypeSkeletonConfig) as input and returns a modified configuration object. + * + * @param {number} level + * - 转换器的优先级。优先级较高的转换器会先执行。 + * - The priority level of the transducer. Transducers with higher priority levels are executed first. + * + * @param {string} [id] + * - (可选)转换器的唯一标识符。用于在需要时引用或操作特定的转换器。 + * - (Optional) A unique identifier for the transducer. Used for referencing or manipulating a specific transducer when needed. + */ + registerConfigTransducer(transducer: IPublicTypeConfigTransducer, level: number, id?: string): void; +} diff --git a/packages/types/src/shell/api/workspace.ts b/packages/types/src/shell/api/workspace.ts new file mode 100644 index 0000000000..b6e7d84cb7 --- /dev/null +++ b/packages/types/src/shell/api/workspace.ts @@ -0,0 +1,79 @@ +import { IPublicModelWindow } from '../model'; +import { IPublicApiPlugins, IPublicApiSkeleton, IPublicModelResource, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType } from '@alilc/lowcode-types'; + +export interface IPublicApiWorkspace< + Plugins = IPublicApiPlugins, + Skeleton = IPublicApiSkeleton, + ModelWindow = IPublicModelWindow, + Resource = IPublicModelResource, +> { + + /** 是否启用 workspace 模式 */ + isActive: boolean; + + /** 当前设计器窗口 */ + window: ModelWindow | null; + + plugins: Plugins; + + skeleton: Skeleton; + + /** 当前设计器的编辑窗口 */ + windows: ModelWindow[]; + + /** 获取资源树列表 */ + get resourceList(): IPublicModelResource[]; + + /** 设置资源树列表 */ + setResourceList(resourceList: IPublicResourceList): void; + + /** 资源树列表更新事件 */ + onResourceListChange(fn: (resourceList: IPublicResourceList) => void): IPublicTypeDisposable; + + /** 注册资源 */ + registerResourceType(resourceTypeModel: IPublicTypeResourceType): void; + + /** + * 打开视图窗口 + * @deprecated + */ + openEditorWindow(resourceName: string, id: string, extra: Object, viewName?: string, sleep?: boolean): Promise<void>; + + /** 打开视图窗口 */ + openEditorWindow(resource: Resource, sleep?: boolean): Promise<void>; + + /** 通过视图 id 打开窗口 */ + openEditorWindowById(id: string): void; + + /** + * 移除视图窗口 + * @deprecated + */ + removeEditorWindow(resourceName: string, id: string): void; + + /** + * 移除视图窗口 + */ + removeEditorWindow(resource: Resource): void; + + /** 通过视图 id 移除窗口 */ + removeEditorWindowById(id: string): void; + + /** 窗口新增/删除的事件 */ + onChangeWindows(fn: () => void): IPublicTypeDisposable; + + /** active 窗口变更事件 */ + onChangeActiveWindow(fn: () => void): IPublicTypeDisposable; + + /** + * active 视图变更事件 + * @since v1.1.7 + */ + onChangeActiveEditorView(fn: () => void): IPublicTypeDisposable; + + /** + * window 下的所有视图 renderer ready 事件 + * @since v1.1.7 + */ + onWindowRendererReady(fn: () => void): IPublicTypeDisposable; +} \ No newline at end of file diff --git a/packages/types/src/shell/enum/context-menu.ts b/packages/types/src/shell/enum/context-menu.ts new file mode 100644 index 0000000000..fd209b1974 --- /dev/null +++ b/packages/types/src/shell/enum/context-menu.ts @@ -0,0 +1,7 @@ +export enum IPublicEnumContextMenuType { + SEPARATOR = 'separator', + // 'menuItem' + MENU_ITEM = 'menuItem', + // 'nodeTree' + NODE_TREE = 'nodeTree', +} \ No newline at end of file diff --git a/packages/types/src/shell/enum/drag-object-type.ts b/packages/types/src/shell/enum/drag-object-type.ts new file mode 100644 index 0000000000..c6bacda23d --- /dev/null +++ b/packages/types/src/shell/enum/drag-object-type.ts @@ -0,0 +1,14 @@ +// eslint-disable-next-line no-shadow +export enum IPublicEnumDragObjectType { + // eslint-disable-next-line no-shadow + Node = 'node', + NodeData = 'nodedata', +} + +/** + * @deprecated use IPublicEnumDragObjectType instead + */ +export enum DragObjectType { + Node = IPublicEnumDragObjectType.Node, + NodeData = IPublicEnumDragObjectType.NodeData, +} diff --git a/packages/types/src/shell/enum/event-names.ts b/packages/types/src/shell/enum/event-names.ts new file mode 100644 index 0000000000..1bb8682d44 --- /dev/null +++ b/packages/types/src/shell/enum/event-names.ts @@ -0,0 +1,9 @@ +/** + * 所有公开可用的事件名定义 + * All public event names + * names should be like 'namespace.modelName.whatHappened' + * + */ +// eslint-disable-next-line no-shadow +export enum IPublicEnumEventNames { +} \ No newline at end of file diff --git a/packages/types/src/shell/enum/index.ts b/packages/types/src/shell/enum/index.ts new file mode 100644 index 0000000000..13282d0f2b --- /dev/null +++ b/packages/types/src/shell/enum/index.ts @@ -0,0 +1,7 @@ +export * from './event-names'; +export * from './transition-type'; +export * from './transform-stage'; +export * from './drag-object-type'; +export * from './prop-value-changed-type'; +export * from './plugin-register-level'; +export * from './context-menu'; \ No newline at end of file diff --git a/packages/types/src/shell/enum/plugin-register-level.ts b/packages/types/src/shell/enum/plugin-register-level.ts new file mode 100644 index 0000000000..a0d9b746bb --- /dev/null +++ b/packages/types/src/shell/enum/plugin-register-level.ts @@ -0,0 +1,6 @@ +export enum IPublicEnumPluginRegisterLevel { + Default = 'default', + Workspace = 'workspace', + Resource = 'resource', + EditorView = 'editorView', +} \ No newline at end of file diff --git a/packages/types/src/shell/enum/prop-value-changed-type.ts b/packages/types/src/shell/enum/prop-value-changed-type.ts new file mode 100644 index 0000000000..b261aa3343 --- /dev/null +++ b/packages/types/src/shell/enum/prop-value-changed-type.ts @@ -0,0 +1,25 @@ +// eslint-disable-next-line no-shadow +export enum IPublicEnumPropValueChangedType { + /** + * normal set value + */ + SET_VALUE = 'SET_VALUE', + /** + * value changed caused by sub-prop value change + */ + SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE' +} + +/** + * @deprecated please use IPublicEnumPropValueChangedType + */ +export enum PROP_VALUE_CHANGED_TYPE { + /** + * normal set value + */ + SET_VALUE = 'SET_VALUE', + /** + * value changed caused by sub-prop value change + */ + SUB_VALUE_CHANGE = 'SUB_VALUE_CHANGE' +} diff --git a/packages/types/src/shell/enum/transform-stage.ts b/packages/types/src/shell/enum/transform-stage.ts new file mode 100644 index 0000000000..18c08f3e47 --- /dev/null +++ b/packages/types/src/shell/enum/transform-stage.ts @@ -0,0 +1,19 @@ +export enum IPublicEnumTransformStage { + Render = 'render', + Serilize = 'serilize', + Save = 'save', + Clone = 'clone', + Init = 'init', + Upgrade = 'upgrade', +} +/** + * @deprecated use IPublicEnumTransformStage instead + */ +export enum TransformStage { + Render = 'render', + Serilize = 'serilize', + Save = 'save', + Clone = 'clone', + Init = 'init', + Upgrade = 'upgrade', +} diff --git a/packages/types/src/shell/enum/transition-type.ts b/packages/types/src/shell/enum/transition-type.ts new file mode 100644 index 0000000000..98dbdba8bd --- /dev/null +++ b/packages/types/src/shell/enum/transition-type.ts @@ -0,0 +1,13 @@ +// eslint-disable-next-line no-shadow +export enum IPublicEnumTransitionType { + /** 节点更新后重绘处理 */ + REPAINT +} + +/** + * @deprecated use IPublicEnumTransitionType instead + */ +export enum TransitionType { + /** 节点更新后重绘处理 */ + REPAINT +} \ No newline at end of file diff --git a/packages/types/src/shell/index.ts b/packages/types/src/shell/index.ts new file mode 100644 index 0000000000..c392c1e120 --- /dev/null +++ b/packages/types/src/shell/index.ts @@ -0,0 +1,5 @@ + +export * from './type'; +export * from './api'; +export * from './model'; +export * from './enum'; \ No newline at end of file diff --git a/packages/types/src/shell/model/active-tracker.ts b/packages/types/src/shell/model/active-tracker.ts new file mode 100644 index 0000000000..ac116a9473 --- /dev/null +++ b/packages/types/src/shell/model/active-tracker.ts @@ -0,0 +1,14 @@ +import { IPublicTypeActiveTarget } from '../type'; +import { IPublicModelNode } from './node'; + +export interface IPublicModelActiveTracker { + + /** + * @since 1.1.7 + */ + target: IPublicTypeActiveTarget | null; + + onChange(fn: (target: IPublicTypeActiveTarget) => void): () => void; + + track(node: IPublicModelNode): void; +} diff --git a/packages/types/src/shell/model/clipboard.ts b/packages/types/src/shell/model/clipboard.ts new file mode 100644 index 0000000000..7fdcc4b1c7 --- /dev/null +++ b/packages/types/src/shell/model/clipboard.ts @@ -0,0 +1,25 @@ + +export interface IPublicModelClipboard { + + /** + * 给剪贴板赋值 + * set data to clipboard + * + * @param {*} data + * @since v1.1.0 + */ + setData(data: any): void; + + /** + * 设置剪贴板数据设置的回调 + * set callback for clipboard provide paste data + * + * @param {KeyboardEvent} keyboardEvent + * @param {(data: any, clipboardEvent: ClipboardEvent) => void} cb + * @since v1.1.0 + */ + waitPasteData( + keyboardEvent: KeyboardEvent, + cb: (data: any, clipboardEvent: ClipboardEvent) => void, + ): void; +} diff --git a/packages/types/src/shell/model/component-meta.ts b/packages/types/src/shell/model/component-meta.ts new file mode 100644 index 0000000000..f2b0032a75 --- /dev/null +++ b/packages/types/src/shell/model/component-meta.ts @@ -0,0 +1,115 @@ +import { IPublicTypeNodeSchema, IPublicTypeNodeData, IPublicTypeIconType, IPublicTypeTransformedComponentMetadata, IPublicTypeI18nData, IPublicTypeNpmInfo, IPublicTypeAdvanced, IPublicTypeFieldConfig, IPublicTypeComponentAction } from '../type'; +import { ReactElement } from 'react'; +import { IPublicModelNode } from './node'; + +export interface IPublicModelComponentMeta< + Node = IPublicModelNode +> { + + /** + * 组件名 + * component name + */ + get componentName(): string; + + /** + * 是否是「容器型」组件 + * is container node or not + */ + get isContainer(): boolean; + + /** + * 是否是最小渲染单元。 + * 当组件需要重新渲染时: + * 若为最小渲染单元,则只渲染当前组件, + * 若不为最小渲染单元,则寻找到上层最近的最小渲染单元进行重新渲染,直至根节点。 + * + * check if this is a mininal render unit. + * when a rerender is needed for a component: + * case 'it`s a mininal render unit': only render itself. + * case 'it`s not a mininal render unit': find a mininal render unit to render in + * its ancesters until root node is reached. + */ + get isMinimalRenderUnit(): boolean; + + /** + * 是否为「模态框」组件 + * check if this is a modal component or not. + */ + get isModal(): boolean; + + /** + * 获取用于设置面板显示用的配置 + * get configs for Settings Panel + */ + get configure(): IPublicTypeFieldConfig[]; + + /** + * 标题 + * title for this component + */ + get title(): string | IPublicTypeI18nData | ReactElement; + + /** + * 图标 + * icon config for this component + */ + get icon(): IPublicTypeIconType; + + /** + * 组件 npm 信息 + * npm informations + */ + get npm(): IPublicTypeNpmInfo; + + /** + * 当前组件的可用 Action + * available actions + */ + get availableActions(): IPublicTypeComponentAction[]; + + /** + * 组件元数据中高级配置部分 + * configure.advanced + * @since v1.1.0 + */ + get advanced(): IPublicTypeAdvanced; + + /** + * 设置 npm 信息 + * set method for npm inforamtion + * @param npm + */ + setNpm(npm: IPublicTypeNpmInfo): void; + + /** + * 获取元数据 + * get component metadata + */ + getMetadata(): IPublicTypeTransformedComponentMetadata; + + /** + * 检测当前对应节点是否可被放置在父节点中 + * check if the current node could be placed in parent node + * @param my 当前节点 + * @param parent 父节点 + */ + checkNestingUp(my: Node | IPublicTypeNodeData, parent: any): boolean; + + /** + * 检测目标节点是否可被放置在父节点中 + * check if the target node(s) could be placed in current node + * @param my 当前节点 + * @param parent 父节点 + */ + checkNestingDown( + my: Node | IPublicTypeNodeData, + target: IPublicTypeNodeSchema | Node | IPublicTypeNodeSchema[], + ): boolean; + + /** + * 刷新元数据,会触发元数据的重新解析和刷新 + * refresh metadata + */ + refreshMetadata(): void; +} diff --git a/packages/types/src/shell/model/detecting.ts b/packages/types/src/shell/model/detecting.ts new file mode 100644 index 0000000000..ec6320ad2f --- /dev/null +++ b/packages/types/src/shell/model/detecting.ts @@ -0,0 +1,46 @@ +import { IPublicModelNode } from './'; +import { IPublicTypeDisposable } from '../type'; + +export interface IPublicModelDetecting<Node = IPublicModelNode> { + + /** + * 是否启用 + * check if current detecting is enabled + * @since v1.1.0 + */ + get enable(): boolean; + + /** + * 当前 hover 的节点 + * get current hovering node + * @since v1.0.16 + */ + get current(): Node | null; + + /** + * hover 指定节点 + * capture node with nodeId + * @param id 节点 id + */ + capture(id: string): void; + + /** + * hover 离开指定节点 + * release node with nodeId + * @param id 节点 id + */ + release(id: string): void; + + /** + * 清空 hover 态 + * clear all hover state + */ + leave(): void; + + /** + * hover 节点变化事件 + * set callback which will be called when hovering object changed. + * @since v1.1.0 + */ + onDetectingChange(fn: (node: Node | null) => void): IPublicTypeDisposable; +} diff --git a/packages/types/src/shell/model/document-model.ts b/packages/types/src/shell/model/document-model.ts new file mode 100644 index 0000000000..4c9344eb48 --- /dev/null +++ b/packages/types/src/shell/model/document-model.ts @@ -0,0 +1,236 @@ +import { IPublicTypeRootSchema, IPublicTypeDragNodeDataObject, IPublicTypeDragNodeObject, IPublicTypePropChangeOptions, IPublicTypeDisposable } from '../type'; +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicApiProject } from '../api'; +import { IPublicModelDropLocation, IPublicModelDetecting, IPublicModelNode, IPublicModelSelection, IPublicModelHistory, IPublicModelModalNodesManager } from './'; +import { IPublicTypeNodeData, IPublicTypeNodeSchema, IPublicTypeOnChangeOptions } from '@alilc/lowcode-types'; + +export interface IPublicModelDocumentModel< + Selection = IPublicModelSelection, + History = IPublicModelHistory, + Node = IPublicModelNode, + DropLocation = IPublicModelDropLocation, + ModalNodesManager = IPublicModelModalNodesManager, + Project = IPublicApiProject +> { + + /** + * 节点选中区模型实例 + * instance of selection + */ + selection: Selection; + + /** + * 画布节点 hover 区模型实例 + * instance of detecting + */ + detecting: IPublicModelDetecting; + + /** + * 操作历史模型实例 + * instance of history + */ + history: History; + + /** + * id + */ + get id(): string; + + set id(id); + + /** + * 获取当前文档所属的 project + * get project which this documentModel belongs to + * @returns + */ + get project(): Project; + + /** + * 获取文档的根节点 + * root node of this documentModel + * @returns + */ + get root(): Node | null; + + get focusNode(): Node | null; + + set focusNode(node: Node | null); + + /** + * 获取文档下所有节点 + * @returns + */ + get nodesMap(): Map<string, Node>; + + /** + * 模态节点管理 + * get instance of modalNodesManager + */ + get modalNodesManager(): ModalNodesManager | null; + + /** + * 根据 nodeId 返回 Node 实例 + * get node by nodeId + * @param nodeId + * @returns + */ + getNodeById(nodeId: string): Node | null; + + /** + * 导入 schema + * import schema data + * @param schema + */ + importSchema(schema: IPublicTypeRootSchema): void; + + /** + * 导出 schema + * export schema + * @param stage + * @returns + */ + exportSchema(stage: IPublicEnumTransformStage): IPublicTypeRootSchema | undefined; + + /** + * 插入节点 + * insert a node + */ + insertNode( + parent: Node, + thing: Node | IPublicTypeNodeData, + at?: number | null | undefined, + copy?: boolean | undefined + ): Node | null; + + /** + * 创建一个节点 + * create a node + * @param data + * @returns + */ + createNode<T = Node>(data: IPublicTypeNodeSchema): T | null; + + /** + * 移除指定节点/节点id + * remove a node by node instance or nodeId + * @param idOrNode + */ + removeNode(idOrNode: string | Node): void; + + /** + * componentsMap of documentModel + * @param extraComps + * @returns + */ + getComponentsMap(extraComps?: string[]): any; + + /** + * 检查拖拽放置的目标节点是否可以放置该拖拽对象 + * check if dragOjbect can be put in this dragTarget + * @param dropTarget 拖拽放置的目标节点 + * @param dragObject 拖拽的对象 + * @returns boolean 是否可以放置 + * @since v1.0.16 + */ + checkNesting( + dropTarget: Node, + dragObject: IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject + ): boolean; + + /** + * 当前 document 新增节点事件 + * set callback for event on node is created for a document + */ + onAddNode(fn: (node: Node) => void): IPublicTypeDisposable; + + /** + * 当前 document 新增节点事件,此时节点已经挂载到 document 上 + * set callback for event on node is mounted to canvas + */ + onMountNode(fn: (payload: { node: Node }) => void): IPublicTypeDisposable; + + /** + * 当前 document 删除节点事件 + * set callback for event on node is removed + */ + onRemoveNode(fn: (node: Node) => void): IPublicTypeDisposable; + + /** + * 当前 document 的 hover 变更事件 + * + * set callback for event on detecting changed + */ + onChangeDetecting(fn: (node: Node) => void): IPublicTypeDisposable; + + /** + * 当前 document 的选中变更事件 + * set callback for event on selection changed + */ + onChangeSelection(fn: (ids: string[]) => void): IPublicTypeDisposable; + + /** + * 当前 document 的节点显隐状态变更事件 + * set callback for event on visibility changed for certain node + * @param fn + */ + onChangeNodeVisible(fn: (node: Node, visible: boolean) => void): IPublicTypeDisposable; + + /** + * 当前 document 的节点 children 变更事件 + * @param fn + */ + onChangeNodeChildren(fn: (info: IPublicTypeOnChangeOptions<Node>) => void): IPublicTypeDisposable; + + /** + * 当前 document 节点属性修改事件 + * @param fn + */ + onChangeNodeProp(fn: (info: IPublicTypePropChangeOptions<Node>) => void): IPublicTypeDisposable; + + /** + * import schema event + * @param fn + * @since v1.0.15 + */ + onImportSchema(fn: (schema: IPublicTypeRootSchema) => void): IPublicTypeDisposable; + + /** + * 判断是否当前节点处于被探测状态 + * check is node being detected + * @param node + * @since v1.1.0 + */ + isDetectingNode(node: Node): boolean; + + /** + * 获取当前的 DropLocation 信息 + * get current drop location + * @since v1.1.0 + */ + get dropLocation(): DropLocation | null; + + /** + * 设置当前的 DropLocation 信息 + * set current drop location + * @since v1.1.0 + */ + set dropLocation(loc: DropLocation | null); + + /** + * 设置聚焦节点变化的回调 + * triggered focused node is set mannually from plugin + * @param fn + * @since v1.1.0 + */ + onFocusNodeChanged( + fn: (doc: IPublicModelDocumentModel, focusNode: Node) => void, + ): IPublicTypeDisposable; + + /** + * 设置 DropLocation 变化的回调 + * triggered when drop location changed + * @param fn + * @since v1.1.0 + */ + onDropLocationChanged(fn: (doc: IPublicModelDocumentModel) => void): IPublicTypeDisposable; +} diff --git a/packages/types/src/shell/model/drag-object.ts b/packages/types/src/shell/model/drag-object.ts new file mode 100644 index 0000000000..92d92eca35 --- /dev/null +++ b/packages/types/src/shell/model/drag-object.ts @@ -0,0 +1,11 @@ +import { IPublicEnumDragObjectType } from '../enum'; +import { IPublicTypeNodeSchema } from '../type'; +import { IPublicModelNode } from './node'; + +export class IPublicModelDragObject { + type: IPublicEnumDragObjectType.Node | IPublicEnumDragObjectType.NodeData; + + data: IPublicTypeNodeSchema | IPublicTypeNodeSchema[] | null; + + nodes: (IPublicModelNode | null)[] | null; +} diff --git a/packages/types/src/shell/model/dragon.ts b/packages/types/src/shell/model/dragon.ts new file mode 100644 index 0000000000..917149faf3 --- /dev/null +++ b/packages/types/src/shell/model/dragon.ts @@ -0,0 +1,70 @@ +/* eslint-disable max-len */ +import { IPublicTypeDisposable, IPublicTypeDragNodeDataObject, IPublicTypeDragObject } from '../type'; +import { IPublicModelDragObject, IPublicModelLocateEvent, IPublicModelNode } from './'; + +export interface IPublicModelDragon< + Node = IPublicModelNode, + LocateEvent = IPublicModelLocateEvent +> { + + /** + * 是否正在拖动 + * is dragging or not + */ + get dragging(): boolean; + + /** + * 绑定 dragstart 事件 + * bind a callback function which will be called on dragging start + * @param func + * @returns + */ + onDragstart(func: (e: LocateEvent) => any): IPublicTypeDisposable; + + /** + * 绑定 drag 事件 + * bind a callback function which will be called on dragging + * @param func + * @returns + */ + onDrag(func: (e: LocateEvent) => any): IPublicTypeDisposable; + + /** + * 绑定 dragend 事件 + * bind a callback function which will be called on dragging end + * @param func + * @returns + */ + onDragend(func: (o: { dragObject: IPublicModelDragObject; copy?: boolean }) => any): IPublicTypeDisposable; + + /** + * 设置拖拽监听的区域 shell,以及自定义拖拽转换函数 boost + * set a html element as shell to dragon as monitoring target, and + * set boost function which is used to transform a MouseEvent to type + * IPublicTypeDragNodeDataObject. + * @param shell 拖拽监听的区域 + * @param boost 拖拽转换函数 + */ + from(shell: Element, boost: (e: MouseEvent) => IPublicTypeDragNodeDataObject | null): any; + + /** + * 发射拖拽对象 + * boost your dragObject for dragging(flying) + * + * @param dragObject 拖拽对象 + * @param boostEvent 拖拽初始时事件 + */ + boost(dragObject: IPublicTypeDragObject, boostEvent: MouseEvent | DragEvent, fromRglNode?: Node): void; + + /** + * 添加投放感应区 + * add sensor area + */ + addSensor(sensor: any): void; + + /** + * 移除投放感应 + * remove sensor area + */ + removeSensor(sensor: any): void; +} diff --git a/packages/types/src/shell/model/drop-location.ts b/packages/types/src/shell/model/drop-location.ts new file mode 100644 index 0000000000..e25522bce9 --- /dev/null +++ b/packages/types/src/shell/model/drop-location.ts @@ -0,0 +1,29 @@ +import { IPublicTypeLocationDetail } from '../type'; +import { IPublicModelLocateEvent, IPublicModelNode } from './'; + +export interface IPublicModelDropLocation { + + /** + * 拖拽位置目标 + * get target of dropLocation + */ + get target(): IPublicModelNode | null; + + /** + * 拖拽放置位置详情 + * get detail of dropLocation + */ + get detail(): IPublicTypeLocationDetail; + + /** + * 拖拽放置位置对应的事件 + * get event of dropLocation + */ + get event(): IPublicModelLocateEvent; + + /** + * 获取一份当前对象的克隆 + * get a clone object of current dropLocation + */ + clone(event: IPublicModelLocateEvent): IPublicModelDropLocation; +} diff --git a/packages/types/src/shell/model/editor-view.ts b/packages/types/src/shell/model/editor-view.ts new file mode 100644 index 0000000000..d51e4f9ff2 --- /dev/null +++ b/packages/types/src/shell/model/editor-view.ts @@ -0,0 +1,7 @@ +import { IPublicModelPluginContext } from './plugin-context'; + +export interface IPublicModelEditorView extends IPublicModelPluginContext { + viewName: string; + + viewType: 'editor' | 'webview'; +} \ No newline at end of file diff --git a/packages/types/src/shell/model/editor.ts b/packages/types/src/shell/model/editor.ts new file mode 100644 index 0000000000..e6171f0312 --- /dev/null +++ b/packages/types/src/shell/model/editor.ts @@ -0,0 +1,44 @@ +/* eslint-disable max-len */ +import { EventEmitter } from 'events'; +import StrictEventEmitter from 'strict-event-emitter-types'; +import * as GlobalEvent from '../../event'; +import { IPublicApiEvent } from '../api'; +import { IPublicTypeEditorValueKey, IPublicTypeEditorGetOptions, IPublicTypeEditorGetResult, IPublicTypeEditorRegisterOptions, IPublicTypeAssetsJson } from '../type'; + +export interface IPublicModelEditor extends StrictEventEmitter<EventEmitter, GlobalEvent.EventConfig> { + get: <T = undefined, KeyOrType = any>( + keyOrType: KeyOrType, + opt?: IPublicTypeEditorGetOptions + ) => IPublicTypeEditorGetResult<T, KeyOrType> | undefined; + + has: (keyOrType: IPublicTypeEditorValueKey) => boolean; + + set: (key: IPublicTypeEditorValueKey, data: any) => void | Promise<void>; + + /** + * 获取 keyOrType 一次 + */ + onceGot: <T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>(keyOrType: KeyOrType) => Promise<IPublicTypeEditorGetResult<T, KeyOrType>>; + + /** + * 获取 keyOrType 多次 + */ + onGot: <T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( + keyOrType: KeyOrType, + fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void + ) => () => void; + + /** + * 监听 keyOrType 变化 + */ + onChange: <T = undefined, KeyOrType extends IPublicTypeEditorValueKey = any>( + keyOrType: KeyOrType, + fn: (data: IPublicTypeEditorGetResult<T, KeyOrType>) => void + ) => () => void; + + register: (data: any, key?: IPublicTypeEditorValueKey, options?: IPublicTypeEditorRegisterOptions) => void; + + get eventBus(): IPublicApiEvent; + + setAssets(assets: IPublicTypeAssetsJson): void; +} diff --git a/packages/types/src/shell/model/engine-config.ts b/packages/types/src/shell/model/engine-config.ts new file mode 100644 index 0000000000..c9473cd120 --- /dev/null +++ b/packages/types/src/shell/model/engine-config.ts @@ -0,0 +1,66 @@ +import { IPublicTypeDisposable } from '../type'; +import { IPublicModelPreference } from './'; + +export interface IPublicModelEngineConfig { + + /** + * 判断指定 key 是否有值 + * check if config has certain key configed + * @param key + * @returns + */ + has(key: string): boolean; + + /** + * 获取指定 key 的值 + * get value by key + * @param key + * @param defaultValue + * @returns + */ + get(key: string, defaultValue?: any): any; + + /** + * 设置指定 key 的值 + * set value for certain key + * @param key + * @param value + */ + set(key: string, value: any): void; + + /** + * 批量设值,set 的对象版本 + * set multiple config key-values + * @param config + */ + setConfig(config: { [key: string]: any }): void; + + /** + * 获取指定 key 的值,若此时还未赋值,则等待,若已有值,则直接返回值 + * 注:此函数返回 Promise 实例,只会执行(fullfill)一次 + * wait until value of certain key is set, will only be + * triggered once. + * @param key + * @returns + */ + onceGot(key: string): Promise<any>; + + /** + * 获取指定 key 的值,函数回调模式,若多次被赋值,回调会被多次调用 + * set callback for event of value set for some key + * this will be called each time the value is set + * @param key + * @param fn + * @returns + */ + onGot(key: string, fn: (data: any) => void): IPublicTypeDisposable; + + /** + * 获取全局 Preference, 用于管理全局浏览器侧用户 Preference,如 Panel 是否钉住 + * get global user preference manager, which can be use to store + * user`s preference in user localstorage, such as a panel is pinned or not. + * @returns {IPublicModelPreference} + * @since v1.1.0 + */ + getPreference(): IPublicModelPreference; +} diff --git a/packages/types/src/shell/model/exclusive-group.ts b/packages/types/src/shell/model/exclusive-group.ts new file mode 100644 index 0000000000..b930a13444 --- /dev/null +++ b/packages/types/src/shell/model/exclusive-group.ts @@ -0,0 +1,10 @@ +import { IPublicModelNode, IPublicTypeTitleContent } from '..'; + +export interface IPublicModelExclusiveGroup< + Node = IPublicModelNode, +> { + readonly id: string | undefined; + readonly title: IPublicTypeTitleContent | undefined; + get firstNode(): Node | null; + setVisible(node: Node): void; +} diff --git a/packages/types/src/shell/model/history.ts b/packages/types/src/shell/model/history.ts new file mode 100644 index 0000000000..9d75295ab4 --- /dev/null +++ b/packages/types/src/shell/model/history.ts @@ -0,0 +1,62 @@ +import { IPublicTypeDisposable } from '../type'; + +export interface IPublicModelHistory { + + /** + * 历史记录跳转到指定位置 + * go to a specific history + * @param cursor + */ + go(cursor: number): void; + + /** + * 历史记录后退 + * go backward in history + */ + back(): void; + + /** + * 历史记录前进 + * go forward in history + */ + forward(): void; + + /** + * 保存当前状态 + * do save current change as a record in history + */ + savePoint(): void; + + /** + * 当前是否是「保存点」,即是否有状态变更但未保存 + * check if there is unsaved change for history + */ + isSavePoint(): boolean; + + /** + * 获取 state,判断当前是否为「可回退」、「可前进」的状态 + * get flags in number which indicat current change state + * + * | 1 | 1 | 1 | + * | -------- | -------- | -------- | + * | modified | redoable | undoable | + * eg. + * 7 means : modified && redoable && undoable + * 5 means : modified && undoable + */ + getState(): number; + + /** + * 监听 state 变更事件 + * monitor on stateChange event + * @param func + */ + onChangeState(func: () => any): IPublicTypeDisposable; + + /** + * 监听历史记录游标位置变更事件 + * monitor on cursorChange event + * @param func + */ + onChangeCursor(func: () => any): IPublicTypeDisposable; +} diff --git a/packages/types/src/shell/model/index.ts b/packages/types/src/shell/model/index.ts new file mode 100644 index 0000000000..ffe6347ac2 --- /dev/null +++ b/packages/types/src/shell/model/index.ts @@ -0,0 +1,35 @@ +export * from './component-meta'; +export * from './detecting'; +export * from './document-model'; +export * from './drag-object'; +export * from './dragon'; +export * from './drop-location'; +export * from './history'; +export * from './locate-event'; +export * from './modal-nodes-manager'; +export * from './node-children'; +export * from './node'; +export * from './prop'; +export * from './props'; +export * from './selection'; +export * from './setting-prop-entry'; +export * from './setting-top-entry'; +export * from '../type/plugin'; +export * from './window'; +export * from './scroll-target'; +export * from './scroller'; +export * from './active-tracker'; +export * from './exclusive-group'; +export * from './plugin-context'; +export * from './setting-target'; +export * from './engine-config'; +export * from './editor'; +export * from './preference'; +export * from './plugin-instance'; +export * from './sensor'; +export * from './resource'; +export * from './clipboard'; +export * from './setting-field'; +export * from './editor-view'; +export * from './skeleton-item'; +export * from './simulator-render'; diff --git a/packages/types/src/shell/model/locate-event.ts b/packages/types/src/shell/model/locate-event.ts new file mode 100644 index 0000000000..bb64ab15eb --- /dev/null +++ b/packages/types/src/shell/model/locate-event.ts @@ -0,0 +1,38 @@ +import { IPublicModelDocumentModel, IPublicModelDragObject } from './'; + +export interface IPublicModelLocateEvent { + + get type(): string; + + /** + * 浏览器窗口坐标系 + */ + readonly globalX: number; + readonly globalY: number; + + /** + * 原始事件 + */ + readonly originalEvent: MouseEvent | DragEvent; + + /** + * 浏览器事件响应目标 + */ + target?: Element | null; + + canvasX?: number; + + canvasY?: number; + + /** + * 事件订正标识,初始构造时,从发起端构造,缺少 canvasX,canvasY, 需要经过订正才有 + */ + fixed?: true; + + /** + * 激活或目标文档 + */ + documentModel?: IPublicModelDocumentModel | null; + + get dragObject(): IPublicModelDragObject | null; +} diff --git a/packages/types/src/shell/model/modal-nodes-manager.ts b/packages/types/src/shell/model/modal-nodes-manager.ts new file mode 100644 index 0000000000..07656c0701 --- /dev/null +++ b/packages/types/src/shell/model/modal-nodes-manager.ts @@ -0,0 +1,42 @@ +import { IPublicModelNode } from './'; + +export interface IPublicModelModalNodesManager<Node = IPublicModelNode> { + + /** + * 设置模态节点,触发内部事件 + * set modal nodes, trigger internal events + */ + setNodes(): void; + + /** + * 获取模态节点(们) + * get modal nodes + */ + getModalNodes(): Node[]; + + /** + * 获取当前可见的模态节点 + * get current visible modal node + */ + getVisibleModalNode(): Node | null; + + /** + * 隐藏模态节点(们) + * hide modal nodes + */ + hideModalNodes(): void; + + /** + * 设置指定节点为可见态 + * set specfic model node as visible + * @param node Node + */ + setVisible(node: Node): void; + + /** + * 设置指定节点为不可见态 + * set specfic model node as invisible + * @param node Node + */ + setInvisible(node: Node): void; +} diff --git a/packages/types/src/shell/model/node-children.ts b/packages/types/src/shell/model/node-children.ts new file mode 100644 index 0000000000..f2be13250b --- /dev/null +++ b/packages/types/src/shell/model/node-children.ts @@ -0,0 +1,190 @@ +import { IPublicTypeNodeSchema, IPublicTypeNodeData } from '../type'; +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicModelNode } from './'; + +export interface IPublicModelNodeChildren< + Node = IPublicModelNode +> { + + /** + * 返回当前 children 实例所属的节点实例 + * get owner node of this nodeChildren + */ + get owner(): Node | null; + + /** + * children 内的节点实例数 + * get count of child nodes + */ + get size(): number; + + /** + * @deprecated please use isEmptyNode + * 是否为空 + * @returns + */ + get isEmpty(): boolean; + + /** + * 是否为空 + * + * @returns + */ + get isEmptyNode(): boolean; + + /** + * @deprecated please use notEmptyNode + * judge if it is not empty + */ + get notEmpty(): boolean; + + /** + * judge if it is not empty + */ + get notEmptyNode(): boolean; + + /** + * 删除指定节点 + * + * delete the node + * @param node + */ + delete(node: Node): boolean; + + /** + * 插入一个节点 + * + * insert a node at specific position + * @param node 待插入节点 + * @param at 插入下标 + * @returns + */ + insert(node: Node, at?: number | null): void; + + /** + * 返回指定节点的下标 + * + * get index of node in current children + * @param node + * @returns + */ + indexOf(node: Node): number; + + /** + * 类似数组 splice 操作 + * + * provide the same function with {Array.prototype.splice} + * @param start + * @param deleteCount + * @param node + */ + splice(start: number, deleteCount: number, node?: Node): any; + + /** + * 返回指定下标的节点 + * + * get node with index + * @param index + * @returns + */ + get(index: number): Node | null; + + /** + * 是否包含指定节点 + * + * check if node exists in current children + * @param node + * @returns + */ + has(node: Node): boolean; + + /** + * 类似数组的 forEach + * + * provide the same function with {Array.prototype.forEach} + * @param fn + */ + forEach(fn: (node: Node, index: number) => void): void; + + /** + * 类似数组的 reverse + * + * provide the same function with {Array.prototype.reverse} + */ + reverse(): Node[]; + + /** + * 类似数组的 map + * + * provide the same function with {Array.prototype.map} + * @param fn + */ + map<T = any>(fn: (node: Node, index: number) => T): T[] | null; + + /** + * 类似数组的 every + * provide the same function with {Array.prototype.every} + * @param fn + */ + every(fn: (node: Node, index: number) => boolean): boolean; + + /** + * 类似数组的 some + * provide the same function with {Array.prototype.some} + * @param fn + */ + some(fn: (node: Node, index: number) => boolean): boolean; + + /** + * 类似数组的 filter + * provide the same function with {Array.prototype.filter} + * @param fn + */ + filter(fn: (node: Node, index: number) => boolean): any; + + /** + * 类似数组的 find + * provide the same function with {Array.prototype.find} + * @param fn + */ + find(fn: (node: Node, index: number) => boolean): Node | null | undefined; + + /** + * 类似数组的 reduce + * + * provide the same function with {Array.prototype.reduce} + * @param fn + */ + reduce(fn: (acc: any, cur: Node) => any, initialValue: any): void; + + /** + * 导入 schema + * + * import schema + * @param data + */ + importSchema(data?: IPublicTypeNodeData | IPublicTypeNodeData[]): void; + + /** + * 导出 schema + * + * export schema + * @param stage + */ + exportSchema(stage: IPublicEnumTransformStage): IPublicTypeNodeSchema; + + /** + * 执行新增、删除、排序等操作 + * + * excute remove/add/sort operations + * @param remover + * @param adder + * @param sorter + */ + mergeChildren( + remover: (node: Node, idx: number) => boolean, + adder: (children: Node[]) => IPublicTypeNodeData[] | null, + sorter: (firstNode: Node, secondNode: Node) => number + ): any; + +} diff --git a/packages/types/src/shell/model/node.ts b/packages/types/src/shell/model/node.ts new file mode 100644 index 0000000000..9d8cec3647 --- /dev/null +++ b/packages/types/src/shell/model/node.ts @@ -0,0 +1,507 @@ +import { ReactElement } from 'react'; +import { IPublicTypeNodeSchema, IPublicTypeIconType, IPublicTypeI18nData, IPublicTypeCompositeValue, IPublicTypePropsMap, IPublicTypePropsList } from '../type'; +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicModelNodeChildren, IPublicModelComponentMeta, IPublicModelProp, IPublicModelProps, IPublicModelSettingTopEntry, IPublicModelDocumentModel, IPublicModelExclusiveGroup } from './'; + +export interface IBaseModelNode< + Document = IPublicModelDocumentModel, + Node = IPublicModelNode, + NodeChildren = IPublicModelNodeChildren, + ComponentMeta = IPublicModelComponentMeta, + SettingTopEntry = IPublicModelSettingTopEntry, + Props = IPublicModelProps, + Prop = IPublicModelProp, + ExclusiveGroup = IPublicModelExclusiveGroup +> { + + /** + * 节点 id + * node id + */ + id: string; + + /** + * 节点标题 + * title of node + */ + get title(): string | IPublicTypeI18nData | ReactElement; + + /** + * @deprecated please use isContainerNode + */ + get isContainer(): boolean; + + /** + * 是否为「容器型」节点 + * check if node is a container type node + * @since v1.1.0 + */ + get isContainerNode(): boolean; + + /** + * @deprecated please use isRootNode + */ + get isRoot(): boolean; + + /** + * 是否为根节点 + * check if node is root in the tree + * @since v1.1.0 + */ + get isRootNode(): boolean; + + /** + * @deprecated please use isEmptyNode + */ + get isEmpty(): boolean; + + /** + * 是否为空节点(无 children 或者 children 为空) + * check if current node is empty, which means no children or children is empty + * @since v1.1.0 + */ + get isEmptyNode(): boolean; + + /** + * @deprecated please use isPageNode + * 是否为 Page 节点 + */ + get isPage(): boolean; + + /** + * 是否为 Page 节点 + * check if node is Page + * @since v1.1.0 + */ + get isPageNode(): boolean; + + /** + * @deprecated please use isComponentNode + */ + get isComponent(): boolean; + + /** + * 是否为 Component 节点 + * check if node is Component + * @since v1.1.0 + */ + get isComponentNode(): boolean; + + /** + * @deprecated please use isModalNode + */ + get isModal(): boolean; + + /** + * 是否为「模态框」节点 + * check if node is Modal + * @since v1.1.0 + */ + get isModalNode(): boolean; + + /** + * @deprecated please use isSlotNode + */ + get isSlot(): boolean; + + /** + * 是否为插槽节点 + * check if node is a Slot + * @since v1.1.0 + */ + get isSlotNode(): boolean; + + /** + * @deprecated please use isParentalNode + */ + get isParental(): boolean; + + /** + * 是否为父类/分支节点 + * check if node a parental node + * @since v1.1.0 + */ + get isParentalNode(): boolean; + + /** + * @deprecated please use isLeafNode + */ + get isLeaf(): boolean; + + /** + * 是否为叶子节点 + * check if node is a leaf node in tree + * @since v1.1.0 + */ + get isLeafNode(): boolean; + + /** + * 获取当前节点的锁定状态 + * check if current node is locked + * @since v1.0.16 + */ + get isLocked(): boolean; + + /** + * @deprecated please use isRGLContainerNode + */ + set isRGLContainer(flag: boolean); + + /** + * @deprecated please use isRGLContainerNode + * @returns Boolean + */ + get isRGLContainer(); + + /** + * 设置为磁贴布局节点 + * @since v1.1.0 + */ + set isRGLContainerNode(flag: boolean); + + /** + * 获取磁贴布局节点设置状态 + * @returns Boolean + * @since v1.1.0 + */ + get isRGLContainerNode(); + + /** + * 下标 + * index + */ + get index(): number | undefined; + + /** + * 图标 + * get icon of this node + */ + get icon(): IPublicTypeIconType; + + /** + * 节点所在树的层级深度,根节点深度为 0 + * depth level of this node, value of root node is 0 + */ + get zLevel(): number; + + /** + * 节点 componentName + * componentName + */ + get componentName(): string; + + /** + * 节点的物料元数据 + * get component meta of this node + */ + get componentMeta(): ComponentMeta | null; + + /** + * 获取节点所属的文档模型对象 + * get documentModel of this node + */ + get document(): Document | null; + + /** + * 获取当前节点的前一个兄弟节点 + * get previous sibling of this node + */ + get prevSibling(): Node | null | undefined; + + /** + * 获取当前节点的后一个兄弟节点 + * get next sibling of this node + */ + get nextSibling(): Node | null | undefined; + + /** + * 获取当前节点的父亲节点 + * get parent of this node + */ + get parent(): Node | null; + + /** + * 获取当前节点的孩子节点模型 + * get children of this node + */ + get children(): NodeChildren | null; + + /** + * 节点上挂载的插槽节点们 + * get slots of this node + */ + get slots(): Node[]; + + /** + * 当前节点为插槽节点时,返回节点对应的属性实例 + * return coresponding prop when this node is a slot node + */ + get slotFor(): Prop | null | undefined; + + /** + * 返回节点的属性集 + * get props + */ + get props(): Props | null; + + /** + * 返回节点的属性集 + * get props data + */ + get propsData(): IPublicTypePropsMap | IPublicTypePropsList | null; + + /** + * get conditionGroup + */ + get conditionGroup(): ExclusiveGroup | null; + + /** + * 获取符合搭建协议 - 节点 schema 结构 + * get schema of this node + * @since v1.1.0 + */ + get schema(): IPublicTypeNodeSchema; + + /** + * 获取对应的 setting entry + * get setting entry of this node + * @since v1.1.0 + */ + get settingEntry(): SettingTopEntry; + + /** + * 返回节点的尺寸、位置信息 + * get rect information for this node + */ + getRect(): DOMRect | null; + + /** + * 是否有挂载插槽节点 + * check if current node has slots + */ + hasSlots(): boolean; + + /** + * 是否设定了渲染条件 + * check if current node has condition value set + */ + hasCondition(): boolean; + + /** + * 是否设定了循环数据 + * check if loop is set for this node + */ + hasLoop(): boolean; + + /** + * 获取指定 path 的属性模型实例 + * get prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param createIfNone 如果不存在,是否新建,默认为 true + */ + getProp(path: string | number, createIfNone?: boolean): Prop | null; + + /** + * 获取指定 path 的属性模型实例值 + * get prop value by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getPropValue(path: string): any; + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * + * get extra prop by path, an extra prop means a prop not exists in the `props` + * but as siblint of the `props` + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param createIfNone 当没有属性的时候,是否创建一个属性 + */ + getExtraProp(path: string, createIfNone?: boolean): Prop | null; + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * + * get extra prop value by path, an extra prop means a prop not exists in the `props` + * but as siblint of the `props` + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @returns + */ + getExtraPropValue(path: string): any; + + /** + * 设置指定 path 的属性模型实例值 + * set value for prop with path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setPropValue(path: string | number, value: IPublicTypeCompositeValue): void; + + /** + * 设置指定 path 的属性模型实例值 + * set value for extra prop with path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void; + + /** + * 导入节点数据 + * import node schema + * @param data + */ + importSchema(data: IPublicTypeNodeSchema): void; + + /** + * 导出节点数据 + * export schema from this node + * @param stage + * @param options + */ + exportSchema(stage: IPublicEnumTransformStage, options?: any): IPublicTypeNodeSchema; + + /** + * 在指定位置之前插入一个节点 + * insert a node befor current node + * @param node + * @param ref + * @param useMutator + */ + insertBefore( + node: Node, + ref?: Node | undefined, + useMutator?: boolean, + ): void; + + /** + * 在指定位置之后插入一个节点 + * insert a node after this node + * @param node + * @param ref + * @param useMutator + */ + insertAfter( + node: Node, + ref?: Node | undefined, + useMutator?: boolean, + ): void; + + /** + * 替换指定节点 + * replace a child node with data provided + * @param node 待替换的子节点 + * @param data 用作替换的节点对象或者节点描述 + * @returns + */ + replaceChild(node: Node, data: any): Node | null; + + /** + * 将当前节点替换成指定节点描述 + * replace current node with a new node schema + * @param schema + */ + replaceWith(schema: IPublicTypeNodeSchema): any; + + /** + * 选中当前节点实例 + * select current node + */ + select(): void; + + /** + * 设置悬停态 + * set hover value for current node + * @param flag + */ + hover(flag: boolean): void; + + /** + * 设置节点锁定状态 + * set lock value for current node + * @param flag + * @since v1.0.16 + */ + lock(flag?: boolean): void; + + /** + * 删除当前节点实例 + * remove current node + */ + remove(): void; + + /** + * 执行新增、删除、排序等操作 + * excute remove/add/sort operations on node`s children + * + * @since v1.1.0 + */ + mergeChildren( + remover: (node: Node, idx: number) => boolean, + adder: (children: Node[]) => any, + sorter: (firstNode: Node, secondNode: Node) => number + ): any; + + /** + * 当前节点是否包含某子节点 + * check if current node contains another node as a child + * @param node + * @since v1.1.0 + */ + contains(node: Node): boolean; + + /** + * 是否可执行某 action + * check if current node can perform certain aciton with actionName + * @param actionName action 名字 + * @since v1.1.0 + */ + canPerformAction(actionName: string): boolean; + + /** + * 当前节点是否可见 + * check if current node is visible + * @since v1.1.0 + */ + get visible(): boolean; + + /** + * 设置当前节点是否可见 + * set visible value for current node + * @since v1.1.0 + */ + set visible(value: boolean); + + /** + * 获取该节点的 ConditionalVisible 值 + * check if current node ConditionalVisible + * @since v1.1.0 + */ + isConditionalVisible(): boolean | undefined; + + /** + * 设置该节点的 ConditionalVisible 为 true + * make this node as conditionalVisible === true + * @since v1.1.0 + */ + setConditionalVisible(): void; + + /** + * 获取节点实例对应的 dom 节点 + */ + getDOMNode(): HTMLElement; + + /** + * 获取磁贴相关信息 + */ + getRGL(): { + isContainerNode: boolean; + isEmptyNode: boolean; + isRGLContainerNode: boolean; + isRGLNode: boolean; + isRGL: boolean; + rglNode: Node | null; + }; +} + +export interface IPublicModelNode extends IBaseModelNode<IPublicModelDocumentModel, IPublicModelNode> {} \ No newline at end of file diff --git a/packages/types/src/shell/model/plugin-context.ts b/packages/types/src/shell/model/plugin-context.ts new file mode 100644 index 0000000000..d4d715e96b --- /dev/null +++ b/packages/types/src/shell/model/plugin-context.ts @@ -0,0 +1,131 @@ +import { + IPublicApiSkeleton, + IPublicApiHotkey, + IPublicApiSetters, + IPublicApiMaterial, + IPublicApiEvent, + IPublicApiProject, + IPublicApiCommon, + IPublicApiLogger, + IPublicApiCanvas, + IPluginPreferenceMananger, + IPublicApiPlugins, + IPublicApiWorkspace, + IPublicApiCommonUI, + IPublicApiCommand, +} from '../api'; +import { IPublicEnumPluginRegisterLevel } from '../enum'; +import { IPublicModelEngineConfig, IPublicModelWindow } from './'; + +export interface IPublicModelPluginContext { + + /** + * 可通过该对象读取插件初始化配置 + * by using this, init options can be accessed from inside plugin + */ + preference: IPluginPreferenceMananger; + + /** + * skeleton API + * @tutorial https://lowcode-engine.cn/site/docs/api/skeleton + */ + get skeleton(): IPublicApiSkeleton; + + /** + * hotkey API + * @tutorial https://lowcode-engine.cn/site/docs/api/hotkey + */ + get hotkey(): IPublicApiHotkey; + + /** + * setter API + * @tutorial https://lowcode-engine.cn/site/docs/api/setters + */ + get setters(): IPublicApiSetters; + + /** + * config API + * @tutorial https://lowcode-engine.cn/site/docs/api/config + */ + get config(): IPublicModelEngineConfig; + + /** + * material API + * @tutorial https://lowcode-engine.cn/site/docs/api/material + */ + get material(): IPublicApiMaterial; + + /** + * event API + * this event works globally, can be used between plugins and engine. + * @tutorial https://lowcode-engine.cn/site/docs/api/event + */ + get event(): IPublicApiEvent; + + /** + * project API + * @tutorial https://lowcode-engine.cn/site/docs/api/project + */ + get project(): IPublicApiProject; + + /** + * common API + * @tutorial https://lowcode-engine.cn/site/docs/api/common + */ + get common(): IPublicApiCommon; + + /** + * plugins API + * @tutorial https://lowcode-engine.cn/site/docs/api/plugins + */ + get plugins(): IPublicApiPlugins; + + /** + * logger API + * @tutorial https://lowcode-engine.cn/site/docs/api/logger + */ + get logger(): IPublicApiLogger; + + /** + * this event works within current plugin, on an emit locally. + * @tutorial https://lowcode-engine.cn/site/docs/api/event + */ + get pluginEvent(): IPublicApiEvent; + + /** + * canvas API + * @tutorial https://lowcode-engine.cn/site/docs/api/canvas + */ + get canvas(): IPublicApiCanvas; + + /** + * workspace API + * @tutorial https://lowcode-engine.cn/site/docs/api/workspace + */ + get workspace(): IPublicApiWorkspace; + + /** + * commonUI API + * @tutorial https://lowcode-engine.cn/site/docs/api/commonUI + */ + get commonUI(): IPublicApiCommonUI; + + get command(): IPublicApiCommand; + + /** + * 插件注册层级 + * @since v1.1.7 + */ + get registerLevel(): IPublicEnumPluginRegisterLevel; + + get isPluginRegisteredInWorkspace(): boolean; + + get editorWindow(): IPublicModelWindow; +} + +/** + * @deprecated please use IPublicModelPluginContext instead + */ +export interface ILowCodePluginContext extends IPublicModelPluginContext { + +} diff --git a/packages/types/src/shell/model/plugin-instance.ts b/packages/types/src/shell/model/plugin-instance.ts new file mode 100644 index 0000000000..88904205d0 --- /dev/null +++ b/packages/types/src/shell/model/plugin-instance.ts @@ -0,0 +1,28 @@ +import { IPublicTypePluginMeta } from '../type/plugin-meta'; + +export interface IPublicModelPluginInstance { + + /** + * 是否 disable + * current plugin instance is disabled or not + */ + disabled: boolean; + + /** + * 插件名称 + * plugin name + */ + get pluginName(): string; + + /** + * 依赖信息,依赖的其他插件 + * depenency info + */ + get dep(): string[]; + + /** + * 插件配置元数据 + * meta info of this plugin + */ + get meta(): IPublicTypePluginMeta; +} diff --git a/packages/types/src/shell/model/preference.ts b/packages/types/src/shell/model/preference.ts new file mode 100644 index 0000000000..e200dae9db --- /dev/null +++ b/packages/types/src/shell/model/preference.ts @@ -0,0 +1,18 @@ + +export interface IPublicModelPreference { + + /** + * set value from local storage by module and key + */ + set(key: string, value: any, module?: string): void; + + /** + * get value from local storage by module and key + */ + get(key: string, module: string): any; + + /** + * check if local storage contain certain key + */ + contains(key: string, module: string): boolean; +} diff --git a/packages/types/src/shell/model/prop.ts b/packages/types/src/shell/model/prop.ts new file mode 100644 index 0000000000..71442e64ab --- /dev/null +++ b/packages/types/src/shell/model/prop.ts @@ -0,0 +1,72 @@ +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicTypeCompositeValue } from '../type'; +import { IPublicModelNode } from './'; + +export interface IPublicModelProp< + Node = IPublicModelNode +> { + + /** + * id + */ + get id(): string; + + /** + * key 值 + * get key of prop + */ + get key(): string | number | undefined; + + /** + * 返回当前 prop 的路径 + * get path of current prop + */ + get path(): string[]; + + /** + * 返回所属的节点实例 + * get node instance, which this prop belongs to + */ + get node(): Node | null; + + /** + * 当本 prop 代表一个 Slot 时,返回对应的 slotNode + * return the slot node (only if the current prop represents a slot) + * @since v1.1.0 + */ + get slotNode(): Node | undefined | null; + + /** + * 是否是 Prop , 固定返回 true + * check if it is a prop or not, and of course always return true + * @experimental + */ + get isProp(): boolean; + + /** + * 设置值 + * set value for this prop + * @param val + */ + setValue(val: IPublicTypeCompositeValue): void; + + /** + * 获取值 + * get value of this prop + */ + getValue(): any; + + /** + * 移除值 + * remove value of this prop + * @since v1.0.16 + */ + remove(): void; + + /** + * 导出值 + * export schema + * @param stage + */ + exportSchema(stage: IPublicEnumTransformStage): IPublicTypeCompositeValue; +} diff --git a/packages/types/src/shell/model/props.ts b/packages/types/src/shell/model/props.ts new file mode 100644 index 0000000000..f3ef2e4519 --- /dev/null +++ b/packages/types/src/shell/model/props.ts @@ -0,0 +1,89 @@ +import { IPublicTypeCompositeValue } from '../type'; +import { IPublicModelNode, IPublicModelProp } from './'; + +export interface IBaseModelProps< + Prop +> { + + /** + * id + */ + get id(): string; + + /** + * 返回当前 props 的路径 + * return path of current props + */ + get path(): string[]; + + /** + * 返回所属的 node 实例 + */ + get node(): IPublicModelNode | null; + + /** + * 获取指定 path 的属性模型实例 + * get prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getProp(path: string): Prop | null; + + /** + * 获取指定 path 的属性模型实例值 + * get value of prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getPropValue(path: string): any; + + /** + * 获取指定 path 的属性模型实例, + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * get extra prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getExtraProp(path: string): Prop | null; + + /** + * 获取指定 path 的属性模型实例值 + * 注:导出时,不同于普通属性,该属性并不挂载在 props 之下,而是与 props 同级 + * get value of extra prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + */ + getExtraPropValue(path: string): any; + + /** + * 设置指定 path 的属性模型实例值 + * set value of prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setPropValue(path: string, value: IPublicTypeCompositeValue): void; + + /** + * 设置指定 path 的属性模型实例值 + * set value of extra prop by path + * @param path 属性路径,支持 a / a.b / a.0 等格式 + * @param value 值 + */ + setExtraPropValue(path: string, value: IPublicTypeCompositeValue): void; + + /** + * 当前 props 是否包含某 prop + * check if the specified key is existing or not. + * @param key + * @since v1.1.0 + */ + has(key: string): boolean; + + /** + * 添加一个 prop + * add a key with given value + * @param value + * @param key + * @since v1.1.0 + */ + add(value: IPublicTypeCompositeValue, key?: string | number | undefined): any; + +} + +export interface IPublicModelProps extends IBaseModelProps<IPublicModelProp> {}; \ No newline at end of file diff --git a/packages/types/src/shell/model/resource.ts b/packages/types/src/shell/model/resource.ts new file mode 100644 index 0000000000..acd7d056f5 --- /dev/null +++ b/packages/types/src/shell/model/resource.ts @@ -0,0 +1,31 @@ +import { ReactElement } from 'react'; + +export interface IBaseModelResource< + Resource +> { + get title(): string | undefined; + + get id(): string | undefined; + + get icon(): ReactElement | undefined; + + get options(): Record<string, any>; + + get name(): string | undefined; + + get type(): string | undefined; + + get category(): string | undefined; + + get children(): Resource[]; + + get viewName(): string | undefined; + + get description(): string | undefined; + + get config(): { + [key: string]: any; + } | undefined; +} + +export type IPublicModelResource = IBaseModelResource<IPublicModelResource>; diff --git a/packages/types/src/shell/model/scroll-target.ts b/packages/types/src/shell/model/scroll-target.ts new file mode 100644 index 0000000000..1dbbaeeda7 --- /dev/null +++ b/packages/types/src/shell/model/scroll-target.ts @@ -0,0 +1,9 @@ + +export interface IPublicModelScrollTarget { + get left(): number; + get top(): number; + scrollTo(options: { left?: number; top?: number }): void; + scrollToXY(x: number, y: number): void; + get scrollHeight(): number; + get scrollWidth(): number; +} diff --git a/packages/types/src/shell/model/scroller.ts b/packages/types/src/shell/model/scroller.ts new file mode 100644 index 0000000000..7c1a438400 --- /dev/null +++ b/packages/types/src/shell/model/scroller.ts @@ -0,0 +1,8 @@ +export interface IPublicModelScroller { + + scrollTo(options: { left?: number; top?: number }): void; + + cancel(): void; + + scrolling(point: { globalX: number; globalY: number }): void; +} \ No newline at end of file diff --git a/packages/types/src/shell/model/selection.ts b/packages/types/src/shell/model/selection.ts new file mode 100644 index 0000000000..317a49837d --- /dev/null +++ b/packages/types/src/shell/model/selection.ts @@ -0,0 +1,85 @@ +import { IPublicModelNode } from './'; +import { IPublicTypeDisposable } from '../type'; + +export interface IPublicModelSelection< + Node = IPublicModelNode +> { + + /** + * 返回选中的节点 id + * get ids of selected nodes + */ + get selected(): string[]; + + /** + * 返回选中的节点(如多个节点只返回第一个) + * return selected Node instance,return the first one if multiple nodes are selected + * @since v1.1.0 + */ + get node(): Node | null; + + /** + * 选中指定节点(覆盖方式) + * select node with id, this will override current selection + * @param id + */ + select(id: string): void; + + /** + * 批量选中指定节点们 + * select node with ids, this will override current selection + * + * @param ids + */ + selectAll(ids: string[]): void; + + /** + * 移除选中的指定节点 + * remove node from selection with node id + * @param id + */ + remove(id: string): void; + + /** + * 清除所有选中节点 + * clear current selection + */ + clear(): void; + + /** + * 判断是否选中了指定节点 + * check if node with specific id is selected + * @param id + */ + has(id: string): boolean; + + /** + * 选中指定节点(增量方式) + * add node with specific id to selection + * @param id + */ + add(id: string): void; + + /** + * 获取选中的节点实例 + * get selected nodes + */ + getNodes(): Node[]; + + /** + * 获取选区的顶层节点 + * get seleted top nodes + * for example: + * getNodes() returns [A, subA, B], then + * getTopNodes() will return [A, B], subA will be removed + * @since v1.0.16 + */ + getTopNodes(includeRoot?: boolean): Node[]; + + /** + * 注册 selection 变化事件回调 + * set callback which will be called when selection is changed + * @since v1.1.0 + */ + onSelectionChange(fn: (ids: string[]) => void): IPublicTypeDisposable; +} diff --git a/packages/types/src/shell/model/sensor.ts b/packages/types/src/shell/model/sensor.ts new file mode 100644 index 0000000000..b563cddb14 --- /dev/null +++ b/packages/types/src/shell/model/sensor.ts @@ -0,0 +1,45 @@ +import { IPublicTypeNodeInstance } from '../type/node-instance'; +import { + IPublicModelLocateEvent, + IPublicModelDropLocation, + IPublicTypeComponentInstance, + IPublicModelNode, +} from '..'; + +/** + * 拖拽敏感板 + */ +export interface IPublicModelSensor< + Node = IPublicModelNode +> { + + /** + * 是否可响应,比如面板被隐藏,可设置该值 false + */ + readonly sensorAvailable: boolean; + + /** + * 给事件打补丁 + */ + fixEvent(e: IPublicModelLocateEvent): IPublicModelLocateEvent; + + /** + * 定位并激活 + */ + locate(e: IPublicModelLocateEvent): IPublicModelDropLocation | undefined | null; + + /** + * 是否进入敏感板区域 + */ + isEnter(e: IPublicModelLocateEvent): boolean; + + /** + * 取消激活 + */ + deactiveSensor(): void; + + /** + * 获取节点实例 + */ + getNodeInstanceFromElement?: (e: Element | null) => IPublicTypeNodeInstance<IPublicTypeComponentInstance, Node> | null; +} diff --git a/packages/types/src/shell/model/setting-field.ts b/packages/types/src/shell/model/setting-field.ts new file mode 100644 index 0000000000..a011fc21f0 --- /dev/null +++ b/packages/types/src/shell/model/setting-field.ts @@ -0,0 +1,198 @@ +import { IPublicTypeCustomView, IPublicTypeCompositeValue, IPublicTypeSetterType, IPublicTypeSetValueOptions, IPublicTypeFieldConfig, IPublicTypeFieldExtraProps, IPublicTypeDisposable } from '../type'; +import { IPublicModelNode, IPublicModelComponentMeta, IPublicModelSettingTopEntry } from './'; + +export interface IBaseModelSettingField< + SettingTopEntry, + SettingField, + ComponentMeta, + Node +> { + + /** + * 获取设置属性的父设置属性 + */ + readonly parent: SettingTopEntry | SettingField; + + /** + * 获取设置属性的 isGroup + */ + get isGroup(): boolean; + + /** + * 获取设置属性的 id + */ + get id(): string; + + /** + * 获取设置属性的 name + */ + get name(): string | number | undefined; + + /** + * 获取设置属性的 key + */ + get key(): string | number | undefined; + + /** + * 获取设置属性的 path + */ + get path(): (string | number)[]; + + /** + * 获取设置属性的 title + */ + get title(): string; + + /** + * 获取设置属性的 setter + */ + get setter(): IPublicTypeSetterType | null; + + /** + * 获取设置属性的 expanded + */ + get expanded(): boolean; + + /** + * 获取设置属性的 extraProps + */ + get extraProps(): IPublicTypeFieldExtraProps; + + get props(): SettingTopEntry; + + /** + * 获取设置属性对应的节点实例 + */ + get node(): Node | null; + + /** + * 获取顶级设置属性 + */ + get top(): SettingTopEntry; + + /** + * 是否是 SettingField 实例 + */ + get isSettingField(): boolean; + + /** + * componentMeta + */ + get componentMeta(): ComponentMeta | null; + + /** + * 获取设置属性的 items + */ + get items(): Array<SettingField | IPublicTypeCustomView>; + + /** + * 设置 key 值 + * @param key + */ + setKey(key: string | number): void; + + /** + * 设置值 + * @param val 值 + */ + setValue(val: IPublicTypeCompositeValue, extraOptions?: IPublicTypeSetValueOptions): void; + + /** + * 设置子级属性值 + * @param propName 子属性名 + * @param value 值 + */ + setPropValue(propName: string | number, value: any): void; + + /** + * 清空指定属性值 + * @param propName + */ + clearPropValue(propName: string | number): void; + + /** + * 获取配置的默认值 + * @returns + */ + getDefaultValue(): any; + + /** + * 获取值 + * @returns + */ + getValue(): any; + + /** + * 获取子级属性值 + * @param propName 子属性名 + * @returns + */ + getPropValue(propName: string | number): any; + + /** + * 获取顶层附属属性值 + */ + getExtraPropValue(propName: string): any; + + /** + * 设置顶层附属属性值 + */ + setExtraPropValue(propName: string, value: any): void; + + /** + * 获取设置属性集 + * @returns + */ + getProps(): SettingTopEntry; + + /** + * 是否绑定了变量 + * @returns + */ + isUseVariable(): boolean; + + /** + * 设置绑定变量 + * @param flag + */ + setUseVariable(flag: boolean): void; + + /** + * 创建一个设置 field 实例 + * @param config + * @returns + */ + createField(config: IPublicTypeFieldConfig): SettingField; + + /** + * 获取值,当为变量时,返回 mock + * @returns + */ + getMockOrValue(): any; + + /** + * 销毁当前 field 实例 + */ + purge(): void; + + /** + * 移除当前 field 实例 + */ + remove(): void; + + /** + * 设置 autorun + * @param action + * @returns + */ + onEffect(action: () => void): IPublicTypeDisposable; +} + +export interface IPublicModelSettingField extends IBaseModelSettingField< + IPublicModelSettingTopEntry, + IPublicModelSettingField, + IPublicModelComponentMeta, + IPublicModelNode +> { + +} \ No newline at end of file diff --git a/packages/types/src/shell/model/setting-prop-entry.ts b/packages/types/src/shell/model/setting-prop-entry.ts new file mode 100644 index 0000000000..b40eab8bff --- /dev/null +++ b/packages/types/src/shell/model/setting-prop-entry.ts @@ -0,0 +1,6 @@ +import { IPublicModelSettingField } from './'; + +/** + * @deprecated please use IPublicModelSettingField + */ +export type IPublicModelSettingPropEntry = IPublicModelSettingField diff --git a/packages/types/src/shell/model/setting-target.ts b/packages/types/src/shell/model/setting-target.ts new file mode 100644 index 0000000000..6df1e36e99 --- /dev/null +++ b/packages/types/src/shell/model/setting-target.ts @@ -0,0 +1,6 @@ +import { IPublicModelSettingField } from './'; + +/** + * @deprecated please use IPublicModelSettingField + */ +export type IPublicModelSettingTarget = IPublicModelSettingField; diff --git a/packages/types/src/shell/model/setting-top-entry.ts b/packages/types/src/shell/model/setting-top-entry.ts new file mode 100644 index 0000000000..1c3d6c2f15 --- /dev/null +++ b/packages/types/src/shell/model/setting-top-entry.ts @@ -0,0 +1,39 @@ +import { IPublicModelNode, IPublicModelSettingField } from './'; + +export interface IPublicModelSettingTopEntry< + Node = IPublicModelNode, + SettingField = IPublicModelSettingField +> { + + /** + * 返回所属的节点实例 + */ + get node(): Node | null; + + /** + * 获取子级属性对象 + * @param propName + * @returns + */ + get(propName: string | number): SettingField | null; + + /** + * 获取指定 propName 的值 + * @param propName + * @returns + */ + getPropValue(propName: string | number): any; + + /** + * 设置指定 propName 的值 + * @param propName + * @param value + */ + setPropValue(propName: string | number, value: any): void; + + /** + * 清除指定 propName 的值 + * @param propName + */ + clearPropValue(propName: string | number): void; +} diff --git a/packages/types/src/shell/model/simulator-render.ts b/packages/types/src/shell/model/simulator-render.ts new file mode 100644 index 0000000000..8cf3a03c55 --- /dev/null +++ b/packages/types/src/shell/model/simulator-render.ts @@ -0,0 +1,14 @@ +export interface IPublicModelSimulatorRender { + + /** + * 画布组件列表 + */ + components: { + [key: string]: any; + }; + + /** + * 触发画布重新渲染 + */ + rerender: () => void; +} diff --git a/packages/types/src/shell/model/skeleton-item.ts b/packages/types/src/shell/model/skeleton-item.ts new file mode 100644 index 0000000000..beb18f2228 --- /dev/null +++ b/packages/types/src/shell/model/skeleton-item.ts @@ -0,0 +1,21 @@ +/** + * @since 1.1.7 + */ +export interface IPublicModelSkeletonItem { + name: string; + + visible: boolean; + + disable(): void; + + enable(): void; + + hide(): void; + + show(): void; + + /** + * @since v1.1.10 + */ + toggle(): void; +} \ No newline at end of file diff --git a/packages/types/src/shell/model/window.ts b/packages/types/src/shell/model/window.ts new file mode 100644 index 0000000000..95ab738bc1 --- /dev/null +++ b/packages/types/src/shell/model/window.ts @@ -0,0 +1,51 @@ +import { ReactElement } from 'react'; +import { IPublicTypeDisposable, IPublicTypeNodeSchema } from '../type'; +import { IPublicModelResource } from './resource'; +import { IPublicModelEditorView } from './editor-view'; + +export interface IPublicModelWindow< + Resource = IPublicModelResource +> { + + /** 窗口 id */ + id: string; + + /** 窗口标题 */ + title?: string; + + /** 窗口 icon */ + icon?: ReactElement; + + /** 窗口资源类型 */ + resource?: Resource; + + /** + * 窗口当前视图 + * @since v1.1.7 + */ + currentEditorView: IPublicModelEditorView | null; + + /** + * 窗口全部视图实例 + * @since v1.1.7 + */ + editorViews: IPublicModelEditorView[]; + + /** 当前窗口导入 schema */ + importSchema(schema: IPublicTypeNodeSchema): void; + + /** 修改当前窗口视图类型 */ + changeViewType(viewName: string): void; + + /** 调用当前窗口视图保存钩子 */ + save(): Promise<any>; + + /** 窗口视图变更事件 */ + onChangeViewType(fn: (viewName: string) => void): IPublicTypeDisposable; + + /** + * 窗口视图保存事件 + * @since 1.1.7 + */ + onSave(fn: () => void): IPublicTypeDisposable; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/action-content-object.ts b/packages/types/src/shell/type/action-content-object.ts new file mode 100644 index 0000000000..cf3e0c0481 --- /dev/null +++ b/packages/types/src/shell/type/action-content-object.ts @@ -0,0 +1,23 @@ +import { IPublicModelNode } from '../model'; +import { IPublicTypeIconType, TipContent } from './'; + +/** + * 动作描述 + */ +export interface IPublicTypeActionContentObject { + + /** + * 图标 + */ + icon?: IPublicTypeIconType; + + /** + * 描述 + */ + title?: TipContent; + + /** + * 执行动作 + */ + action?: (currentNode: IPublicModelNode) => void; +} diff --git a/packages/types/src/shell/type/active-target.ts b/packages/types/src/shell/type/active-target.ts new file mode 100644 index 0000000000..97845160be --- /dev/null +++ b/packages/types/src/shell/type/active-target.ts @@ -0,0 +1,8 @@ +import { IPublicModelNode } from '../model'; +import { IPublicTypeLocationDetail, IPublicTypeComponentInstance } from './'; + +export interface IPublicTypeActiveTarget { + node: IPublicModelNode; + detail?: IPublicTypeLocationDetail; + instance?: IPublicTypeComponentInstance; +} diff --git a/packages/types/src/shell/type/advanced.ts b/packages/types/src/shell/type/advanced.ts new file mode 100644 index 0000000000..8a6db85b67 --- /dev/null +++ b/packages/types/src/shell/type/advanced.ts @@ -0,0 +1,105 @@ +import { ComponentType, ReactElement } from 'react'; +import { IPublicTypeNodeData, IPublicTypeSnippet, IPublicTypeInitialItem, IPublicTypeFilterItem, IPublicTypeAutorunItem, IPublicTypeCallbacks, IPublicTypeLiveTextEditingConfig } from './'; +import { IPublicModelNode, IPublicModelSettingField } from '../model'; + +/** + * 高级特性配置 + */ +export interface IPublicTypeAdvanced { + + /** + * 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 + * callbacks/hooks which can be used to do + * things on some special ocations like onNodeAdd or onResize + */ + callbacks?: IPublicTypeCallbacks; + + /** + * 拖入容器时,自动带入 children 列表 + */ + initialChildren?: IPublicTypeNodeData[] | ((target: IPublicModelNode) => IPublicTypeNodeData[]); + + /** + * 样式 及 位置,handle 上必须有明确的标识以便事件路由判断,或者主动设置事件独占模式 + * NWSE 是交给引擎计算放置位置,ReactElement 必须自己控制初始位置 + * + * 用于配置设计器中组件 resize 操作工具的样式和内容 + * - hover 时控制柄高亮 + * - mousedown 时请求独占 + * - dragstart 请求通用 resizing 控制 请求 hud 显示 + * - drag 时 计算并设置效果,更新控制柄位置 + */ + getResizingHandlers?: ( + currentNode: any + ) => (Array<{ + type: 'N' | 'W' | 'S' | 'E' | 'NW' | 'NE' | 'SE' | 'SW'; + content?: ReactElement; + propTarget?: string; + appearOn?: 'mouse-enter' | 'mouse-hover' | 'selected' | 'always'; + }> | + ReactElement[]); + + /** + * @deprecated 用于动态初始化拖拽到设计器里的组件的 prop 的值 + */ + initials?: IPublicTypeInitialItem[]; + + /** + * @deprecated 使用组件 metadata 上的 snippets 字段即可 + */ + snippets?: IPublicTypeSnippet[]; + + /** + * 是否绝对布局容器,还未进入协议 + * @experimental not in spec yet + */ + isAbsoluteLayoutContainer?: boolean; + + /** + * hide bem tools when selected + * @experimental not in spec yet + */ + hideSelectTools?: boolean; + + /** + * Live Text Editing:如果 children 内容是纯文本,支持双击直接编辑 + * @experimental not in spec yet + */ + liveTextEditing?: IPublicTypeLiveTextEditingConfig[]; + + /** + * TODO: 补充文档 + * @experimental not in spec yet + */ + view?: ComponentType<any>; + + /** + * @legacy capability for vision + * @deprecated + */ + isTopFixed?: boolean; + + /** + * TODO: 补充文档 或 删除 + * @deprecated not used anywhere, dont know what is it for + */ + context?: { [contextInfoName: string]: any }; + + /** + * @legacy capability for vision + * @deprecated + */ + filters?: IPublicTypeFilterItem[]; + + /** + * @legacy capability for vision + * @deprecated + */ + autoruns?: IPublicTypeAutorunItem[]; + + /** + * @legacy capability for vision + * @deprecated + */ + transducers?: any; +} diff --git a/packages/types/src/shell/type/app-config.ts b/packages/types/src/shell/type/app-config.ts new file mode 100644 index 0000000000..2bcb5f47a9 --- /dev/null +++ b/packages/types/src/shell/type/app-config.ts @@ -0,0 +1,18 @@ +export interface IPublicTypeAppConfig { + sdkVersion?: string; + historyMode?: string; + targetRootID?: string; + layout?: IPublicTypeLayout; + theme?: IPublicTypeTheme; +} + +interface IPublicTypeTheme { + package: string; + version: string; + primary: string; +} + +interface IPublicTypeLayout { + componentName?: string; + props?: Record<string, any>; +} diff --git a/packages/types/src/shell/type/assets-json.ts b/packages/types/src/shell/type/assets-json.ts new file mode 100644 index 0000000000..e00349779a --- /dev/null +++ b/packages/types/src/shell/type/assets-json.ts @@ -0,0 +1,34 @@ +import { IPublicTypeComponentSort, IPublicTypePackage, IPublicTypeRemoteComponentDescription, IPublicTypeComponentDescription } from './'; + +/** + * 资产包协议 + */ + +export interface IPublicTypeAssetsJson { + /** + * 资产包协议版本号 + */ + version: string; + /** + * 大包列表,external 与 package 的概念相似,融合在一起 + */ + packages?: IPublicTypePackage[]; + /** + * 所有组件的描述协议列表所有组件的列表 + */ + components: Array<IPublicTypeComponentDescription | IPublicTypeRemoteComponentDescription>; + /** + * 组件分类列表,用来描述物料面板 + * @deprecated 最新版物料面板已不需要此描述 + */ + componentList?: any[]; + /** + * 业务组件分类列表,用来描述物料面板 + * @deprecated 最新版物料面板已不需要此描述 + */ + bizComponentList?: any[]; + /** + * 用于描述组件面板中的 tab 和 category + */ + sort?: IPublicTypeComponentSort; +} diff --git a/packages/types/src/shell/type/block-schema.ts b/packages/types/src/shell/type/block-schema.ts new file mode 100644 index 0000000000..118a4f8c70 --- /dev/null +++ b/packages/types/src/shell/type/block-schema.ts @@ -0,0 +1,10 @@ +import { IPublicTypeContainerSchema } from './'; + +/** + * 区块容器 + * @see https://lowcode-engine.cn/lowcode + */ + +export interface IPublicTypeBlockSchema extends IPublicTypeContainerSchema { + componentName: 'Block'; +} 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<IPublicTypeCommand, 'name' | 'description' | 'parameters'> { +} \ No newline at end of file diff --git a/packages/types/src/shell/type/component-action.ts b/packages/types/src/shell/type/component-action.ts new file mode 100644 index 0000000000..f86324e7ac --- /dev/null +++ b/packages/types/src/shell/type/component-action.ts @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; +import { IPublicTypeActionContentObject } from './'; + +/** + * @todo 工具条动作 + */ + +export interface IPublicTypeComponentAction { + + /** + * behaviorName + */ + name: string; + + /** + * 菜单名称 + */ + content: string | ReactNode | IPublicTypeActionContentObject; + + /** + * 子集 + */ + items?: IPublicTypeComponentAction[]; + + /** + * 显示与否 + * always: 无法禁用 + */ + condition?: boolean | ((currentNode: any) => boolean) | 'always'; + + /** + * 显示在工具条上 + */ + important?: boolean; +} diff --git a/packages/types/src/shell/type/component-description.ts b/packages/types/src/shell/type/component-description.ts new file mode 100644 index 0000000000..7ee7f5f004 --- /dev/null +++ b/packages/types/src/shell/type/component-description.ts @@ -0,0 +1,16 @@ +import { IPublicTypeComponentMetadata, IPublicTypeReference } from './'; + +/** + * 本地物料描述 + */ + +export interface IPublicTypeComponentDescription extends IPublicTypeComponentMetadata { + /** + * @todo 待补充文档 @jinchan + */ + keywords: string[]; + /** + * 替代 npm 字段的升级版本 + */ + reference?: IPublicTypeReference; +} diff --git a/packages/types/src/shell/type/component-instance.ts b/packages/types/src/shell/type/component-instance.ts new file mode 100644 index 0000000000..4c3716f5d0 --- /dev/null +++ b/packages/types/src/shell/type/component-instance.ts @@ -0,0 +1,6 @@ + +import { Component as ReactComponent } from 'react'; +/** + * 组件实例定义 + */ +export type IPublicTypeComponentInstance = Element | ReactComponent<any> | object; \ No newline at end of file diff --git a/packages/types/src/shell/type/component-metadata.ts b/packages/types/src/shell/type/component-metadata.ts new file mode 100644 index 0000000000..69dc36c309 --- /dev/null +++ b/packages/types/src/shell/type/component-metadata.ts @@ -0,0 +1,101 @@ +import { IPublicTypeIconType, IPublicTypeNpmInfo, IPublicTypeFieldConfig, IPublicTypeI18nData, IPublicTypeComponentSchema, IPublicTypeTitleContent, IPublicTypePropConfig, IPublicTypeConfigure, IPublicTypeAdvanced, IPublicTypeSnippet } from './'; + +/** + * 组件 meta 配置 + */ + +export interface IPublicTypeComponentMetadata { + + /** 其他扩展协议 */ + [key: string]: any; + + /** + * 组件名 + */ + componentName: string; + + /** + * unique id + */ + uri?: string; + + /** + * title or description + */ + title?: IPublicTypeTitleContent; + + /** + * svg icon for component + */ + icon?: IPublicTypeIconType; + + /** + * 组件标签 + */ + tags?: string[]; + + /** + * 组件描述 + */ + description?: string; + + /** + * 组件文档链接 + */ + docUrl?: string; + + /** + * 组件快照 + */ + screenshot?: string; + + /** + * 组件研发模式 + */ + devMode?: 'proCode' | 'lowCode'; + + /** + * npm 源引入完整描述对象 + */ + npm?: IPublicTypeNpmInfo; + + /** + * 组件属性信息 + */ + props?: IPublicTypePropConfig[]; + + /** + * 编辑体验增强 + */ + configure?: IPublicTypeFieldConfig[] | IPublicTypeConfigure; + + /** + * @deprecated, use advanced instead + */ + experimental?: IPublicTypeAdvanced; + + /** + * @todo 待补充文档 + */ + schema?: IPublicTypeComponentSchema; + + /** + * 可用片段 + */ + snippets?: IPublicTypeSnippet[]; + + /** + * 一级分组 + */ + group?: string | IPublicTypeI18nData; + + /** + * 二级分组 + */ + category?: string | IPublicTypeI18nData; + + /** + * 组件优先级排序 + */ + priority?: number; +} diff --git a/packages/types/src/shell/type/component-schema.ts b/packages/types/src/shell/type/component-schema.ts new file mode 100644 index 0000000000..41daae5c34 --- /dev/null +++ b/packages/types/src/shell/type/component-schema.ts @@ -0,0 +1,10 @@ +import { IPublicTypeContainerSchema } from './'; + +/** + * 低代码业务组件容器 + * @see https://lowcode-engine.cn/lowcode + */ + +export interface IPublicTypeComponentSchema extends IPublicTypeContainerSchema { + componentName: 'Component'; +} diff --git a/packages/types/src/shell/type/component-sort.ts b/packages/types/src/shell/type/component-sort.ts new file mode 100644 index 0000000000..add821ea98 --- /dev/null +++ b/packages/types/src/shell/type/component-sort.ts @@ -0,0 +1,14 @@ +/** + * 用于描述组件面板中的 tab 和 category + */ + +export interface IPublicTypeComponentSort { + /** + * 用于描述组件面板的 tab 项及其排序,例如:["精选组件", "原子组件"] + */ + groupList?: string[]; + /** + * 组件面板中同一个 tab 下的不同区间用 category 区分,category 的排序依照 categoryList 顺序排列; + */ + categoryList?: string[]; +} diff --git a/packages/types/src/shell/type/composite-value.ts b/packages/types/src/shell/type/composite-value.ts new file mode 100644 index 0000000000..e7aea645ec --- /dev/null +++ b/packages/types/src/shell/type/composite-value.ts @@ -0,0 +1,11 @@ +import { IPublicTypeJSONValue, IPublicTypeJSExpression, IPublicTypeJSFunction, IPublicTypeJSSlot, IPublicTypeCompositeArray, IPublicTypeCompositeObject } from './'; + +/** + * 复合类型 + */ +export type IPublicTypeCompositeValue = IPublicTypeJSONValue | + IPublicTypeJSExpression | + IPublicTypeJSFunction | + IPublicTypeJSSlot | + IPublicTypeCompositeArray | + IPublicTypeCompositeObject; diff --git a/packages/types/src/shell/type/config-transducer.ts b/packages/types/src/shell/type/config-transducer.ts new file mode 100644 index 0000000000..64c33a5c4e --- /dev/null +++ b/packages/types/src/shell/type/config-transducer.ts @@ -0,0 +1,9 @@ +import { IPublicTypeSkeletonConfig } from '.'; + +export interface IPublicTypeConfigTransducer { + (prev: IPublicTypeSkeletonConfig): IPublicTypeSkeletonConfig; + + level?: number; + + id?: string; +} diff --git a/packages/types/src/shell/type/configure.ts b/packages/types/src/shell/type/configure.ts new file mode 100644 index 0000000000..44fd1ffe68 --- /dev/null +++ b/packages/types/src/shell/type/configure.ts @@ -0,0 +1,27 @@ +import { IPublicTypeComponentConfigure, ConfigureSupport, IPublicTypeFieldConfig, IPublicTypeAdvanced } from './'; + +/** + * 编辑体验配置 + */ +export interface IPublicTypeConfigure { + + /** + * 属性面板配置 + */ + props?: IPublicTypeFieldConfig[]; + + /** + * 组件能力配置 + */ + component?: IPublicTypeComponentConfigure; + + /** + * 通用扩展面板支持性配置 + */ + supports?: ConfigureSupport; + + /** + * 高级特性配置 + */ + advanced?: IPublicTypeAdvanced; +} diff --git a/packages/types/src/shell/type/container-schema.ts b/packages/types/src/shell/type/container-schema.ts new file mode 100644 index 0000000000..416788f867 --- /dev/null +++ b/packages/types/src/shell/type/container-schema.ts @@ -0,0 +1,57 @@ +import { InterpretDataSource as DataSource } from '@alilc/lowcode-datasource-types'; +import { + IPublicTypeJSExpression, + IPublicTypeJSFunction, + IPublicTypeCompositeObject, + IPublicTypeCompositeValue, + IPublicTypeNodeSchema, +} from './'; + +/** + * 容器结构描述 + */ +export interface IPublicTypeContainerSchema extends IPublicTypeNodeSchema { + /** + * 'Block' | 'Page' | 'Component'; + */ + componentName: string; + /** + * 文件名称 + */ + fileName: string; + /** + * @todo 待文档定义 + */ + meta?: Record<string, unknown>; + /** + * 容器初始数据 + */ + state?: { + [key: string]: IPublicTypeCompositeValue; + }; + /** + * 自定义方法设置 + */ + methods?: { + [key: string]: IPublicTypeJSExpression | IPublicTypeJSFunction; + }; + /** + * 生命周期对象 + */ + lifeCycles?: { + // @todo 生命周期对象建议改为闭合集合 + [key: string]: IPublicTypeJSExpression | IPublicTypeJSFunction; + }; + /** + * 样式文件 + */ + css?: string; + /** + * 异步数据源配置 + */ + dataSource?: DataSource; + /** + * 低代码业务组件默认属性 + */ + defaultProps?: IPublicTypeCompositeObject; +} diff --git a/packages/types/src/shell/type/context-menu.ts b/packages/types/src/shell/type/context-menu.ts new file mode 100644 index 0000000000..dd6d583c25 --- /dev/null +++ b/packages/types/src/shell/type/context-menu.ts @@ -0,0 +1,63 @@ +import { IPublicEnumContextMenuType } from '../enum'; +import { IPublicModelNode } from '../model'; +import { IPublicTypeI18nData } from './i8n-data'; +import { IPublicTypeHelpTipConfig } from './widget-base-config'; + +export interface IPublicTypeContextMenuItem extends Omit<IPublicTypeContextMenuAction, 'condition' | 'disabled' | 'items'> { + disabled?: boolean; + + items?: Omit<IPublicTypeContextMenuItem, 'items'>[]; +} + +export interface IPublicTypeContextMenuAction { + + /** + * 动作的唯一标识符 + * Unique identifier for the action + */ + name: string; + + /** + * 显示的标题,可以是字符串或国际化数据 + * Display title, can be a string or internationalized data + */ + title?: string | IPublicTypeI18nData; + + /** + * 菜单项类型 + * Menu item type + * @see IPublicEnumContextMenuType + * @default IPublicEnumContextMenuType.MENU_ITEM + */ + type?: IPublicEnumContextMenuType; + + /** + * 点击时执行的动作,可选 + * Action to execute on click, optional + */ + action?: (nodes?: IPublicModelNode[], event?: MouseEvent) => void; + + /** + * 子菜单项或生成子节点的函数,可选,仅支持两级 + * Sub-menu items or function to generate child node, optional + */ + items?: Omit<IPublicTypeContextMenuAction, 'items'>[] | ((nodes?: IPublicModelNode[]) => Omit<IPublicTypeContextMenuAction, 'items'>[]); + + /** + * 显示条件函数 + * Function to determine display condition + */ + condition?: (nodes?: IPublicModelNode[]) => boolean; + + /** + * 禁用条件函数,可选 + * Function to determine disabled condition, optional + */ + disabled?: (nodes?: IPublicModelNode[]) => boolean; + + /** + * 帮助提示,可选 + */ + help?: IPublicTypeHelpTipConfig; +} + diff --git a/packages/types/src/shell/type/custom-view.ts b/packages/types/src/shell/type/custom-view.ts new file mode 100644 index 0000000000..076fd83a3d --- /dev/null +++ b/packages/types/src/shell/type/custom-view.ts @@ -0,0 +1,3 @@ +import { ComponentType, ReactElement } from 'react'; + +export type IPublicTypeCustomView = ReactElement | ComponentType<any>; diff --git a/packages/types/src/shell/type/disposable.ts b/packages/types/src/shell/type/disposable.ts new file mode 100644 index 0000000000..aa0c2ac4dc --- /dev/null +++ b/packages/types/src/shell/type/disposable.ts @@ -0,0 +1,3 @@ +export interface IPublicTypeDisposable { + (): void; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/dom-text.ts b/packages/types/src/shell/type/dom-text.ts new file mode 100644 index 0000000000..3bb227f2d0 --- /dev/null +++ b/packages/types/src/shell/type/dom-text.ts @@ -0,0 +1 @@ +export type IPublicTypeDOMText = string; diff --git a/packages/types/src/shell/type/drag-any-object.ts b/packages/types/src/shell/type/drag-any-object.ts new file mode 100644 index 0000000000..93269945ff --- /dev/null +++ b/packages/types/src/shell/type/drag-any-object.ts @@ -0,0 +1,5 @@ + +export interface IPublicTypeDragAnyObject { + type: string; + [key: string]: any; +} diff --git a/packages/types/src/shell/type/drag-node-data-object.ts b/packages/types/src/shell/type/drag-node-data-object.ts new file mode 100644 index 0000000000..8c6980c514 --- /dev/null +++ b/packages/types/src/shell/type/drag-node-data-object.ts @@ -0,0 +1,10 @@ +import { IPublicTypeNodeSchema } from './'; +import { IPublicEnumDragObjectType } from '../enum'; + +export interface IPublicTypeDragNodeDataObject { + type: IPublicEnumDragObjectType.NodeData; + data: IPublicTypeNodeSchema | IPublicTypeNodeSchema[]; + thumbnail?: string; + description?: string; + [extra: string]: any; +} diff --git a/packages/types/src/shell/type/drag-node-object.ts b/packages/types/src/shell/type/drag-node-object.ts new file mode 100644 index 0000000000..21f14b2bcb --- /dev/null +++ b/packages/types/src/shell/type/drag-node-object.ts @@ -0,0 +1,7 @@ +import { IPublicModelNode } from '..'; +import { IPublicEnumDragObjectType } from '../enum'; + +export interface IPublicTypeDragNodeObject<Node = IPublicModelNode> { + type: IPublicEnumDragObjectType.Node; + nodes: Node[]; +} diff --git a/packages/types/src/shell/type/drag-object.ts b/packages/types/src/shell/type/drag-object.ts new file mode 100644 index 0000000000..1df6ce107c --- /dev/null +++ b/packages/types/src/shell/type/drag-object.ts @@ -0,0 +1,4 @@ +import { IPublicTypeDragNodeDataObject, IPublicTypeDragNodeObject, IPublicTypeDragAnyObject } from './'; + +// eslint-disable-next-line max-len +export type IPublicTypeDragObject = IPublicTypeDragNodeObject | IPublicTypeDragNodeDataObject | IPublicTypeDragAnyObject; diff --git a/packages/types/src/shell/type/dynamic-props.ts b/packages/types/src/shell/type/dynamic-props.ts new file mode 100644 index 0000000000..1d87a65671 --- /dev/null +++ b/packages/types/src/shell/type/dynamic-props.ts @@ -0,0 +1,3 @@ +import { IPublicModelSettingField } from '../model'; + +export type IPublicTypeDynamicProps = (target: IPublicModelSettingField) => Record<string, unknown>; diff --git a/packages/types/src/shell/type/dynamic-setter.ts b/packages/types/src/shell/type/dynamic-setter.ts new file mode 100644 index 0000000000..5883bb2bb9 --- /dev/null +++ b/packages/types/src/shell/type/dynamic-setter.ts @@ -0,0 +1,4 @@ +import { IPublicModelSettingPropEntry, IPublicTypeCustomView } from '..'; +import { IPublicTypeSetterConfig } from './setter-config'; + +export type IPublicTypeDynamicSetter = (target: IPublicModelSettingPropEntry) => (string | IPublicTypeSetterConfig | IPublicTypeCustomView); diff --git a/packages/types/src/shell/type/editor-get-options.ts b/packages/types/src/shell/type/editor-get-options.ts new file mode 100644 index 0000000000..ed5477057d --- /dev/null +++ b/packages/types/src/shell/type/editor-get-options.ts @@ -0,0 +1,5 @@ + +export interface IPublicTypeEditorGetOptions { + forceNew?: boolean; + sourceCls?: new (...args: any[]) => any; +} diff --git a/packages/types/src/shell/type/editor-get-result.ts b/packages/types/src/shell/type/editor-get-result.ts new file mode 100644 index 0000000000..af3639ac04 --- /dev/null +++ b/packages/types/src/shell/type/editor-get-result.ts @@ -0,0 +1,4 @@ + +export type IPublicTypeEditorGetResult<T, ClsType> = T extends undefined ? ClsType extends { + prototype: infer R; +} ? R : any : T; diff --git a/packages/types/src/shell/type/editor-register-options.ts b/packages/types/src/shell/type/editor-register-options.ts new file mode 100644 index 0000000000..3853465813 --- /dev/null +++ b/packages/types/src/shell/type/editor-register-options.ts @@ -0,0 +1,19 @@ +/** + * duck-typed power-di + * + * @see https://www.npmjs.com/package/power-di + */ +export interface IPublicTypeEditorRegisterOptions { + + /** + * default: true + */ + singleton?: boolean; + + /** + * if data a class, auto new a instance. + * if data a function, auto run(lazy). + * default: true + */ + autoNew?: boolean; +} diff --git a/packages/types/src/shell/type/editor-value-key.ts b/packages/types/src/shell/type/editor-value-key.ts new file mode 100644 index 0000000000..8c0d3c6c96 --- /dev/null +++ b/packages/types/src/shell/type/editor-value-key.ts @@ -0,0 +1,2 @@ + +export type IPublicTypeEditorValueKey = (new (...args: any[]) => any) | symbol | string; diff --git a/packages/types/src/shell/type/editor-view-config.ts b/packages/types/src/shell/type/editor-view-config.ts new file mode 100644 index 0000000000..9bb3b75559 --- /dev/null +++ b/packages/types/src/shell/type/editor-view-config.ts @@ -0,0 +1,11 @@ +export interface IPublicEditorViewConfig { + + /** 视图初始化钩子 */ + init?: () => Promise<void>; + + /** 资源保存时,会调用视图的钩子 */ + save?: () => Promise<void>; + + /** viewType 类型为 'webview' 时渲染的地址 */ + url?: () => Promise<string>; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/editor-view.ts b/packages/types/src/shell/type/editor-view.ts new file mode 100644 index 0000000000..2357a48f53 --- /dev/null +++ b/packages/types/src/shell/type/editor-view.ts @@ -0,0 +1,12 @@ +import { IPublicEditorViewConfig } from './editor-view-config'; + +export interface IPublicTypeEditorView { + + /** 资源名字 */ + viewName: string; + + /** 资源类型 */ + viewType?: 'editor' | 'webview'; + + (ctx: any, options: any): IPublicEditorViewConfig; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/engine-options.ts b/packages/types/src/shell/type/engine-options.ts new file mode 100644 index 0000000000..8221c4089c --- /dev/null +++ b/packages/types/src/shell/type/engine-options.ts @@ -0,0 +1,199 @@ +import { RequestHandlersMap } from '@alilc/lowcode-datasource-types'; +import { ComponentType } from 'react'; + +export interface IPublicTypeEngineOptions { + + /** + * 是否开启 condition 的能力,默认在设计器中不管 condition 是啥都正常展示 + * when this is true, node that configured as conditional not renderring + * will not display in canvas. + * @default false + */ + enableCondition?: boolean; + + /** + * TODO: designMode 无法映射到文档渲染模块 + * + * 设计模式,live 模式将会实时展示变量值,默认值:'design' + * + * @default 'design' + * @experimental + */ + designMode?: 'design' | 'live'; + + /** + * 设备类型,默认值:'default' + * @default 'default' + */ + device?: 'default' | 'mobile' | string; + + /** + * 指定初始化的 deviceClassName,挂载到画布的顶层节点上 + */ + deviceClassName?: string; + + /** + * 语言,默认值:'zh-CN' + * @default 'zh-CN' + */ + locale?: string; + + /** + * 渲染器类型,默认值:'react' + */ + renderEnv?: 'react' | string; + + /** + * 设备类型映射器,处理设计器与渲染器中 device 的映射 + */ + deviceMapper?: { + transform: (originalDevice: string) => string; + }; + + /** + * 开启严格插件模式,默认值:STRICT_PLUGIN_MODE_DEFAULT , 严格模式下,插件将无法通过 engineOptions 传递自定义配置项 + * enable strict plugin mode, default value: false + * under strict mode, customed engineOption is not accepted. + */ + enableStrictPluginMode?: boolean; + + /** + * 开启拖拽组件时,即将被放入的容器是否有视觉反馈,默认值:false + */ + enableReactiveContainer?: boolean; + + /** + * 关闭画布自动渲染,在资产包多重异步加载的场景有效,默认值:false + */ + disableAutoRender?: boolean; + + /** + * 关闭拖拽组件时的虚线响应,性能考虑,默认值:false + */ + disableDetecting?: boolean; + + /** + * 定制画布中点击被忽略的 selectors,默认值:undefined + */ + customizeIgnoreSelectors?: (defaultIgnoreSelectors: string[], e: MouseEvent) => string[]; + + /** + * 禁止默认的设置面板,默认值:false + */ + disableDefaultSettingPanel?: boolean; + + /** + * 禁止默认的设置器,默认值:false + */ + disableDefaultSetters?: boolean; + + /** + * 打开画布的锁定操作,默认值:false + */ + enableCanvasLock?: boolean; + + /** + * 容器锁定后,容器本身是否可以设置属性,仅当画布锁定特性开启时生效,默认值为:false + */ + enableLockedNodeSetting?: boolean; + + /** + * 当选中节点切换时,是否停留在相同的设置 tab 上,默认值:false + */ + stayOnTheSameSettingTab?: boolean; + + /** + * 是否在只有一个 item 的时候隐藏设置 tabs,默认值:false + */ + hideSettingsTabsWhenOnlyOneItem?: boolean; + + /** + * 自定义 loading 组件 + */ + loadingComponent?: ComponentType; + + /** + * 设置所有属性支持变量配置,默认值:false + */ + supportVariableGlobally?: boolean; + + /** + * 设置 simulator 相关的 url,默认值:undefined + */ + simulatorUrl?: string[]; + + /** + * Vision-polyfill settings + * @deprecated this exists for some legacy reasons + */ + visionSettings?: { + // 是否禁用降级 reducer,默认值:false + disableCompatibleReducer?: boolean; + // 是否开启在 render 阶段开启 filter reducer,默认值:false + enableFilterReducerInRenderStage?: boolean; + }; + + /** + * 与 react-renderer 的 appHelper 一致,https://lowcode-engine.cn/site/docs/guide/expand/runtime/renderer#apphelper + */ + appHelper?: { + + /** 全局公共函数 */ + utils?: Record<string, any>; + + /** 全局常量 */ + constants?: Record<string, any>; + }; + + /** + * 数据源引擎的请求处理器映射 + */ + requestHandlersMap?: RequestHandlersMap; + + /** + * @default true + * JSExpression 是否只支持使用 this 来访问上下文变量,假如需要兼容原来的 'state.xxx',则设置为 false + */ + thisRequiredInJSE?: boolean; + + /** + * @default false + * 当开启组件未找到严格模式时,渲染模块不会默认给一个容器组件 + */ + enableStrictNotFoundMode?: boolean; + + /** + * 配置指定节点为根组件 + */ + focusNodeSelector?: (rootNode: Node) => Node; + + /** + * 开启应用级设计模式 + */ + enableWorkspaceMode?: boolean; + + /** + * @default true + * 应用级设计模式下,自动打开第一个窗口 + */ + enableAutoOpenFirstWindow?: boolean; + + /** + * @default false + * 开启右键菜单能力 + */ + enableContextMenu?: boolean; + + /** + * @default false + * 隐藏设计器辅助层 + */ + hideComponentAction?: boolean; +} + +/** + * @deprecated use IPublicTypeEngineOptions instead + */ +export interface EngineOptions { + +} \ No newline at end of file diff --git a/packages/types/src/shell/type/field-config.ts b/packages/types/src/shell/type/field-config.ts new file mode 100644 index 0000000000..bd09e7b906 --- /dev/null +++ b/packages/types/src/shell/type/field-config.ts @@ -0,0 +1,51 @@ +import { IPublicTypeTitleContent, IPublicTypeSetterType, IPublicTypeFieldExtraProps, IPublicTypeDynamicSetter } from './'; + +/** + * 属性面板配置 + */ +export interface IPublicTypeFieldConfig extends IPublicTypeFieldExtraProps { + + /** + * 面板配置隶属于单个 field 还是分组 + */ + type?: 'field' | 'group'; + + /** + * the name of this setting field, which used in quickEditor + */ + name?: string | number; + + /** + * the field title + * @default sameas .name + */ + title?: IPublicTypeTitleContent; + + /** + * 单个属性的 setter 配置 + * + * the field body contains when .type = 'field' + */ + setter?: IPublicTypeSetterType | IPublicTypeDynamicSetter; + + /** + * the setting items which group body contains when .type = 'group' + */ + items?: IPublicTypeFieldConfig[]; + + /** + * extra props for field + * 其他配置属性(不做流通要求) + */ + extraProps?: IPublicTypeFieldExtraProps; + + /** + * @deprecated + */ + description?: IPublicTypeTitleContent; + + /** + * @deprecated + */ + isExtends?: boolean; +} diff --git a/packages/types/src/shell/type/field-extra-props.ts b/packages/types/src/shell/type/field-extra-props.ts new file mode 100644 index 0000000000..7aae7e0fe8 --- /dev/null +++ b/packages/types/src/shell/type/field-extra-props.ts @@ -0,0 +1,81 @@ +import { IPublicModelSettingField } from '../model'; +import { IPublicTypeLiveTextEditingConfig } from './'; + +/** + * extra props for field + */ +export interface IPublicTypeFieldExtraProps { + + /** + * 是否必填参数 + */ + isRequired?: boolean; + + /** + * default value of target prop for setter use + */ + defaultValue?: any; + + /** + * get value for field + */ + getValue?: (target: IPublicModelSettingField, fieldValue: any) => any; + + /** + * set value for field + */ + setValue?: (target: IPublicModelSettingField, value: any) => void; + + /** + * the field conditional show, is not set always true + * @default undefined + */ + condition?: (target: IPublicModelSettingField) => boolean; + + /** + * 配置当前 prop 是否忽略默认值处理逻辑,如果返回值是 true 引擎不会处理默认值 + * @returns boolean + */ + ignoreDefaultValue?: (target: IPublicModelSettingField) => boolean; + + /** + * autorun when something change + */ + autorun?: (target: IPublicModelSettingField) => void; + + /** + * default collapsed when display accordion + */ + defaultCollapsed?: boolean; + + /** + * important field + */ + important?: boolean; + + /** + * internal use + */ + forceInline?: number; + + /** + * 是否支持变量配置 + */ + supportVariable?: boolean; + + /** + * compatiable vision display + */ + display?: 'accordion' | 'inline' | 'block' | 'plain' | 'popup' | 'entry'; + + // @todo 这个 omit 是否合理? + /** + * @todo 待补充文档 + */ + liveTextEditing?: Omit<IPublicTypeLiveTextEditingConfig, 'propTarget'>; + + /** + * onChange 事件 + */ + onChange?: (value: any, field: IPublicModelSettingField) => void; +} diff --git a/packages/types/src/shell/type/hotkey-callback-config.ts b/packages/types/src/shell/type/hotkey-callback-config.ts new file mode 100644 index 0000000000..1903d6a845 --- /dev/null +++ b/packages/types/src/shell/type/hotkey-callback-config.ts @@ -0,0 +1,10 @@ +import { IPublicTypeHotkeyCallback } from './'; + +export interface IPublicTypeHotkeyCallbackConfig { + callback: IPublicTypeHotkeyCallback; + modifiers: string[]; + action: string; + seq?: string; + level?: number; + combo?: string; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/hotkey-callback.ts b/packages/types/src/shell/type/hotkey-callback.ts new file mode 100644 index 0000000000..4650b9b5cf --- /dev/null +++ b/packages/types/src/shell/type/hotkey-callback.ts @@ -0,0 +1,2 @@ + +export type IPublicTypeHotkeyCallback = (e: KeyboardEvent, combo?: string) => any | false; diff --git a/packages/types/src/shell/type/hotkey-callbacks.ts b/packages/types/src/shell/type/hotkey-callbacks.ts new file mode 100644 index 0000000000..3fd80a4808 --- /dev/null +++ b/packages/types/src/shell/type/hotkey-callbacks.ts @@ -0,0 +1,5 @@ +import { IPublicTypeHotkeyCallbackConfig } from './'; + +export interface IPublicTypeHotkeyCallbacks { + [key: string]: IPublicTypeHotkeyCallbackConfig[]; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/i18n-map.ts b/packages/types/src/shell/type/i18n-map.ts new file mode 100644 index 0000000000..4d56a20b58 --- /dev/null +++ b/packages/types/src/shell/type/i18n-map.ts @@ -0,0 +1,4 @@ + +export interface IPublicTypeI18nMap { + [lang: string]: { [key: string]: string }; +} diff --git a/packages/types/src/shell/type/i8n-data.ts b/packages/types/src/shell/type/i8n-data.ts new file mode 100644 index 0000000000..764d5f82d0 --- /dev/null +++ b/packages/types/src/shell/type/i8n-data.ts @@ -0,0 +1,7 @@ +import { ReactNode } from 'react'; + +export interface IPublicTypeI18nData { + type: 'i18n'; + intl?: ReactNode; + [key: string]: any; +} diff --git a/packages/types/src/shell/type/icon-config.ts b/packages/types/src/shell/type/icon-config.ts new file mode 100644 index 0000000000..f45fe7d697 --- /dev/null +++ b/packages/types/src/shell/type/icon-config.ts @@ -0,0 +1,6 @@ + +export interface IPublicTypeIconConfig { + type: string; + size?: number | 'small' | 'xxs' | 'xs' | 'medium' | 'large' | 'xl' | 'xxl' | 'xxxl' | 'inherit'; + className?: string; +} diff --git a/packages/types/src/shell/type/icon-type.ts b/packages/types/src/shell/type/icon-type.ts new file mode 100644 index 0000000000..99882556b9 --- /dev/null +++ b/packages/types/src/shell/type/icon-type.ts @@ -0,0 +1,4 @@ +import { ReactElement, ComponentType } from 'react'; +import { IPublicTypeIconConfig } from './'; + +export type IPublicTypeIconType = string | ReactElement | ComponentType<any> | IPublicTypeIconConfig; diff --git a/packages/types/src/shell/type/index.ts b/packages/types/src/shell/type/index.ts new file mode 100644 index 0000000000..76dd389255 --- /dev/null +++ b/packages/types/src/shell/type/index.ts @@ -0,0 +1,96 @@ +// this folder contains all interfaces/types working as type definition +// - some exists as type TypeName +// - some althought exists as interfaces , but there won`t be any class implements them. +// all of above cases will with prefix IPublicType, eg. IPublicTypeSomeName +export * from './location'; +export * from './active-target'; +export * from './component-instance'; +export * from './node-schema'; +export * from './disposable'; +export * from './assets-json'; +export * from './metadata-transducer'; +export * from './component-action'; +export * from './preference-value-type'; +export * from './project-schema'; +export * from './block-schema'; +export * from './component-schema'; +export * from './container-schema'; +export * from './page-schema'; +export * from './root-schema'; +export * from './props-transducer'; +export * from './registered-setter'; +export * from './custom-view'; +export * from './widget-base-config'; +export * from './node-data'; +export * from './icon-type'; +export * from './transformed-component-metadata'; +export * from './i8n-data'; +export * from './npm-info'; +export * from './drag-node-data-object'; +export * from './drag-node-object'; +export * from './prop-change-options'; +export * from './drag-any-object'; +export * from './drag-object'; +export * from './composite-value'; +export * from './props-map'; +export * from './props-list'; +export * from './plugin-config'; +export * from './plugin-declaration-property'; +export * from './plugin-declaration'; +export * from './plugin-meta'; +export * from './plugin-creater'; +export * from './plugin'; +export * from './setter-type'; +export * from './set-value-options'; +export * from './field-config'; +export * from './field-extra-props'; +export * from './component-sort'; +export * from './component-metadata'; +export * from './reference'; +export * from './component-description'; +export * from './remote-component-description'; +export * from './package'; +export * from './action-content-object'; +export * from './title-config'; +export * from './title-content'; +export * from './prop-config'; +export * from './prop-types'; +export * from './snippet'; +export * from './advanced'; +export * from './configure'; +export * from './value-type'; +export * from './tip-content'; +export * from './metadata'; +export * from './dynamic-setter'; +export * from './icon-config'; +export * from './dom-text'; +export * from './i18n-map'; +export * from './app-config'; +export * from './npm'; +export * from './dynamic-props'; +export * from './setter-config'; +export * from './tip-config'; +export * from './widget-config-area'; +export * from './hotkey-callback'; +export * from './plugin-register-options'; +export * from './resource-list'; +export * from './engine-options'; +export * from './on-change-options'; +export * from './slot-schema'; +export * from './node-data-type'; +export * from './node-instance'; +export * from './editor-value-key'; +export * from './editor-get-options'; +export * from './editor-get-result'; +export * from './editor-register-options'; +export * from './editor-view'; +export * from './resource-type'; +export * from './resource-type-config'; +export * from './editor-view-config'; +export * from './hotkey-callback-config'; +export * from './hotkey-callbacks'; +export * from './scrollable'; +export * from './simulator-renderer'; +export * from './config-transducer'; +export * from './context-menu'; +export * from './command'; \ No newline at end of file diff --git a/packages/types/src/shell/type/location.ts b/packages/types/src/shell/type/location.ts new file mode 100644 index 0000000000..4f8b59a7c5 --- /dev/null +++ b/packages/types/src/shell/type/location.ts @@ -0,0 +1,56 @@ +import { IPublicModelNode, IPublicModelLocateEvent } from '../model'; + +// eslint-disable-next-line no-shadow +export enum IPublicTypeLocationDetailType { + Children = 'Children', + Prop = 'Prop', +} + +/** + * @deprecated please use IPublicTypeLocationDetailType + */ +export enum LocationDetailType { + Children = 'Children', + Prop = 'Prop', +} + +export type IPublicTypeRect = DOMRect & { + elements?: Array<Element | Text>; + computed?: boolean; +}; + +export interface IPublicTypeLocationChildrenDetail { + type: IPublicTypeLocationDetailType.Children; + index?: number | null; + + /** + * 是否有效位置 + */ + valid?: boolean; + edge?: DOMRect; + near?: { + node: IPublicModelNode; + pos: 'before' | 'after' | 'replace'; + rect?: IPublicTypeRect; + align?: 'V' | 'H'; + }; + focus?: { type: 'slots' } | { type: 'node'; node: IPublicModelNode }; +} + +export interface IPublicTypeLocationPropDetail { + // cover 形态,高亮 domNode,如果 domNode 为空,取 container 的值 + type: IPublicTypeLocationDetailType.Prop; + name: string; + domNode?: HTMLElement; +} + +export type IPublicTypeLocationDetail = IPublicTypeLocationChildrenDetail | IPublicTypeLocationPropDetail | { [key: string]: any; type: string }; + +export interface IPublicTypeLocationData< + Node = IPublicModelNode +> { + target: Node; // shadowNode | ConditionFlow | ElementNode | RootNode + detail: IPublicTypeLocationDetail; + source: string; + event: IPublicModelLocateEvent; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/metadata-transducer.ts b/packages/types/src/shell/type/metadata-transducer.ts new file mode 100644 index 0000000000..e5e407e368 --- /dev/null +++ b/packages/types/src/shell/type/metadata-transducer.ts @@ -0,0 +1,16 @@ +import { IPublicTypeTransformedComponentMetadata } from './'; + + +export interface IPublicTypeMetadataTransducer { + (prev: IPublicTypeTransformedComponentMetadata): IPublicTypeTransformedComponentMetadata; + /** + * 0 - 9 system + * 10 - 99 builtin-plugin + * 100 - app & plugin + */ + level?: number; + /** + * use to replace TODO + */ + id?: string; +} diff --git a/packages/types/src/shell/type/metadata.ts b/packages/types/src/shell/type/metadata.ts new file mode 100644 index 0000000000..c07d9802e1 --- /dev/null +++ b/packages/types/src/shell/type/metadata.ts @@ -0,0 +1,232 @@ +import { MouseEvent } from 'react'; +import { IPublicTypePropType, IPublicTypeComponentAction } from './'; +import { IPublicModelNode, IPublicModelSettingField } from '../model'; + +/** + * 嵌套控制函数 + */ +export type IPublicTypeNestingFilter = (testNode: any, currentNode: any) => boolean; + +/** + * 嵌套控制 + * 防止错误的节点嵌套,比如 a 嵌套 a, FormField 只能在 Form 容器下,Column 只能在 Table 下等 + */ +export interface IPublicTypeNestingRule { + + /** + * 子级白名单 + */ + childWhitelist?: string[] | string | RegExp | IPublicTypeNestingFilter; + + /** + * 父级白名单 + */ + parentWhitelist?: string[] | string | RegExp | IPublicTypeNestingFilter; + + /** + * 后裔白名单 + */ + descendantWhitelist?: string[] | string | RegExp | IPublicTypeNestingFilter; + + /** + * 后裔黑名单 + */ + descendantBlacklist?: string[] | string | RegExp | IPublicTypeNestingFilter; + + /** + * 祖先白名单 可用来做区域高亮 + */ + ancestorWhitelist?: string[] | string | RegExp | IPublicTypeNestingFilter; +} + +/** + * 组件能力配置 + */ +export interface IPublicTypeComponentConfigure { + + /** + * 是否容器组件 + */ + isContainer?: boolean; + + /** + * 组件是否带浮层,浮层组件拖入设计器时会遮挡画布区域,此时应当辅助一些交互以防止阻挡 + */ + isModal?: boolean; + + /** + * 是否存在渲染的根节点 + */ + isNullNode?: boolean; + + /** + * 组件树描述信息 + */ + descriptor?: string; + + /** + * 嵌套控制:防止错误的节点嵌套 + * 比如 a 嵌套 a, FormField 只能在 Form 容器下,Column 只能在 Table 下等 + */ + nestingRule?: IPublicTypeNestingRule; + + /** + * 是否是最小渲染单元 + * 最小渲染单元下的组件渲染和更新都从单元的根节点开始渲染和更新。如果嵌套了多层最小渲染单元,渲染会从最外层的最小渲染单元开始渲染。 + */ + isMinimalRenderUnit?: boolean; + + /** + * 组件选中框的 cssSelector + */ + rootSelector?: string; + + /** + * 禁用的行为,可以为 `'copy'`, `'move'`, `'remove'` 或它们组成的数组 + */ + disableBehaviors?: string[] | string; + + /** + * 用于详细配置上述操作项的内容 + */ + actions?: IPublicTypeComponentAction[]; +} + +export interface IPublicTypeInitialItem { + name: string; + initial: (target: IPublicModelSettingField, currentValue: any) => any; +} +export interface IPublicTypeFilterItem { + name: string; + filter: (target: IPublicModelSettingField | null, currentValue: any) => any; +} +export interface IPublicTypeAutorunItem { + name: string; + autorun: (target: IPublicModelSettingField | null) => any; +} + +// thinkof Array +/** + * Live Text Editing(如果 children 内容是纯文本,支持双击直接编辑)的可配置项目 + */ +export interface IPublicTypeLiveTextEditingConfig { + + /** + * @todo 待补充文档 + */ + propTarget: string; + + /** + * @todo 待补充文档 + */ + selector?: string; + + /** + * 编辑模式 纯文本 | 段落编辑 | 文章编辑(默认纯文本,无跟随工具条) + * @default 'plaintext' + */ + mode?: 'plaintext' | 'paragraph' | 'article'; + + /** + * 从 contentEditable 获取内容并设置到属性 + */ + onSaveContent?: (content: string, prop: any) => any; +} + +export type ConfigureSupportEvent = string | ConfigureSupportEventConfig; + +export interface ConfigureSupportEventConfig { + name: string; + propType?: IPublicTypePropType; + description?: string; + template?: string; +} + +/** + * 通用扩展面板支持性配置 + */ +export interface ConfigureSupport { + + /** + * 支持事件列表 + */ + events?: ConfigureSupportEvent[]; + + /** + * 支持 className 设置 + */ + className?: boolean; + + /** + * 支持样式设置 + */ + style?: boolean; + + /** + * 支持生命周期设置 + */ + lifecycles?: any[]; + + // general?: boolean; + /** + * 支持循环设置 + */ + loop?: boolean; + + /** + * 支持条件式渲染设置 + */ + condition?: boolean; +} + +/** + * handleResizing + */ + +/** + * 配置 callbacks 可捕获引擎抛出的一些事件,例如 onNodeAdd、onResize 等 + */ +export interface IPublicTypeCallbacks { + // hooks + onMouseDownHook?: (e: MouseEvent, currentNode: IPublicModelNode | null) => any; + onDblClickHook?: (e: MouseEvent, currentNode: IPublicModelNode | null) => any; + onClickHook?: (e: MouseEvent, currentNode: IPublicModelNode | null) => any; + // onLocateHook?: (e: any, currentNode: any) => any; + // onAcceptHook?: (currentNode: any, locationData: any) => any; + onMoveHook?: (currentNode: IPublicModelNode) => boolean; + // thinkof 限制性拖拽 + onHoverHook?: (currentNode: IPublicModelNode) => boolean; + + /** 选中 hook,如果返回值是 false,可以控制组件不可被选中 */ + onSelectHook?: (currentNode: IPublicModelNode) => boolean; + onChildMoveHook?: (childNode: IPublicModelNode, currentNode: IPublicModelNode) => boolean; + + // events + onNodeRemove?: (removedNode: IPublicModelNode | null, currentNode: IPublicModelNode | null) => void; + onNodeAdd?: (addedNode: IPublicModelNode | null, currentNode: IPublicModelNode | null) => void; + onSubtreeModified?: (currentNode: IPublicModelNode, options: any) => void; + onResize?: ( + e: MouseEvent & { + trigger: string; + deltaX?: number; + deltaY?: number; + }, + currentNode: any, + ) => void; + onResizeStart?: ( + e: MouseEvent & { + trigger: string; + deltaX?: number; + deltaY?: number; + }, + currentNode: any, + ) => void; + onResizeEnd?: ( + e: MouseEvent & { + trigger: string; + deltaX?: number; + deltaY?: number; + }, + currentNode: IPublicModelNode, + ) => void; +} diff --git a/packages/types/src/shell/type/node-data-type.ts b/packages/types/src/shell/type/node-data-type.ts new file mode 100644 index 0000000000..d7f68041a9 --- /dev/null +++ b/packages/types/src/shell/type/node-data-type.ts @@ -0,0 +1,3 @@ +import { IPublicTypeNodeData } from './node-data'; + +export type IPublicTypeNodeDataType = IPublicTypeNodeData | IPublicTypeNodeData[]; diff --git a/packages/types/src/shell/type/node-data.ts b/packages/types/src/shell/type/node-data.ts new file mode 100644 index 0000000000..0447c9e2a7 --- /dev/null +++ b/packages/types/src/shell/type/node-data.ts @@ -0,0 +1,3 @@ +import { IPublicTypeJSExpression, IPublicTypeNodeSchema, IPublicTypeDOMText, IPublicTypeI18nData } from './'; + +export type IPublicTypeNodeData = IPublicTypeNodeSchema | IPublicTypeJSExpression | IPublicTypeDOMText | IPublicTypeI18nData; diff --git a/packages/types/src/shell/type/node-instance.ts b/packages/types/src/shell/type/node-instance.ts new file mode 100644 index 0000000000..fab8e672ba --- /dev/null +++ b/packages/types/src/shell/type/node-instance.ts @@ -0,0 +1,11 @@ +import { IPublicTypeComponentInstance, IPublicModelNode } from '..'; + +export interface IPublicTypeNodeInstance< + T = IPublicTypeComponentInstance, + Node = IPublicModelNode +> { + docId: string; + nodeId: string; + instance: T; + node?: Node | null; +} diff --git a/packages/types/src/shell/type/node-schema.ts b/packages/types/src/shell/type/node-schema.ts new file mode 100644 index 0000000000..9cbd0a81ac --- /dev/null +++ b/packages/types/src/shell/type/node-schema.ts @@ -0,0 +1,59 @@ +import { IPublicTypeCompositeValue, IPublicTypePropsMap, IPublicTypeNodeData } from './'; + +// 转换成一个 .jsx 文件内 React Class 类 render 函数返回的 jsx 代码 +/** + * 搭建基础协议 - 单个组件树节点描述 + */ +export interface IPublicTypeNodeSchema { + + id?: string; + + /** + * 组件名称 必填、首字母大写 + */ + componentName: string; + + /** + * 组件属性对象 + */ + props?: { + children?: IPublicTypeNodeData | IPublicTypeNodeData[]; + } & IPublicTypePropsMap; // | PropsList; + + /** + * 渲染条件 + */ + condition?: IPublicTypeCompositeValue; + + /** + * 循环数据 + */ + loop?: IPublicTypeCompositeValue; + + /** + * 循环迭代对象、索引名称 ["item", "index"] + */ + loopArgs?: [string, string]; + + /** + * 子节点 + */ + children?: IPublicTypeNodeData | IPublicTypeNodeData[]; + + /** + * 是否锁定 + */ + isLocked?: boolean; + + // @todo + // ------- future support ----- + conditionGroup?: string; + title?: string; + ignore?: boolean; + locked?: boolean; + hidden?: boolean; + isTopFixed?: boolean; + + /** @experimental 编辑态内部使用 */ + __ctx?: any; +} diff --git a/packages/types/src/shell/type/npm-info.ts b/packages/types/src/shell/type/npm-info.ts new file mode 100644 index 0000000000..e91c39ebc1 --- /dev/null +++ b/packages/types/src/shell/type/npm-info.ts @@ -0,0 +1,33 @@ +/** + * npm 源引入完整描述对象 + */ +export interface IPublicTypeNpmInfo { + /** + * 源码组件名称 + */ + componentName?: string; + /** + * 源码组件库名 + */ + package: string; + /** + * 源码组件版本号 + */ + version?: string; + /** + * 是否解构 + */ + destructuring?: boolean; + /** + * 源码组件名称 + */ + exportName?: string; + /** + * 子组件名 + */ + subName?: string; + /** + * 组件路径 + */ + main?: string; +} diff --git a/packages/types/src/shell/type/npm.ts b/packages/types/src/shell/type/npm.ts new file mode 100644 index 0000000000..2d1396be4f --- /dev/null +++ b/packages/types/src/shell/type/npm.ts @@ -0,0 +1,16 @@ +import { IPublicTypeNpmInfo } from './npm-info'; + +export interface IPublicTypeLowCodeComponent { + /** + * 研发模式 + */ + devMode: 'lowCode'; + /** + * 组件名称 + */ + componentName: string; +} + +export type IPublicTypeProCodeComponent = IPublicTypeNpmInfo; +export type IPublicTypeComponentMap = IPublicTypeProCodeComponent | IPublicTypeLowCodeComponent; +export type IPublicTypeComponentsMap = IPublicTypeComponentMap[]; diff --git a/packages/types/src/shell/type/on-change-options.ts b/packages/types/src/shell/type/on-change-options.ts new file mode 100644 index 0000000000..47b88d72f7 --- /dev/null +++ b/packages/types/src/shell/type/on-change-options.ts @@ -0,0 +1,8 @@ +import { IPublicModelNode } from '..'; + +export interface IPublicTypeOnChangeOptions< + Node = IPublicModelNode +> { + type: string; + node: Node; +} diff --git a/packages/types/src/shell/type/package.ts b/packages/types/src/shell/type/package.ts new file mode 100644 index 0000000000..b33fa3f94a --- /dev/null +++ b/packages/types/src/shell/type/package.ts @@ -0,0 +1,55 @@ +import { EitherOr } from '../../utils'; +import { IPublicTypeComponentSchema, IPublicTypeProjectSchema } from './'; + +/** + * 定义组件大包及 external 资源的信息 + * 应该被编辑器默认加载 + */ +export type IPublicTypePackage = EitherOr<{ + /** + * npm 包名 + */ + package: string; + /** + * 包唯一标识 + */ + id: string; + /** + * 包版本号 + */ + version: string; + /** + * 组件渲染态视图打包后的 CDN url 列表,包含 js 和 css + */ + urls?: string[] | any; + /** + * 组件编辑态视图打包后的 CDN url 列表,包含 js 和 css + */ + editUrls?: string[] | any; + /** + * 作为全局变量引用时的名称,和webpack output.library字段含义一样,用来定义全局变量名 + */ + library: string; + /** + * @experimental + * + * TODO: 需推进提案 @度城 + */ + async?: boolean; + /** + * 标识当前 package 从其他 package 的导出方式 + */ + exportMode?: 'functionCall'; + /** + * 标识当前 package 是从 window 上的哪个属性导出来的 + */ + exportSourceLibrary?: any; + /** + * 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; + */ + exportName?: string; + /** + * 低代码组件 schema 内容 + */ + schema?: IPublicTypeProjectSchema<IPublicTypeComponentSchema>; +}, 'package', 'id'>; diff --git a/packages/types/src/shell/type/page-schema.ts b/packages/types/src/shell/type/page-schema.ts new file mode 100644 index 0000000000..670c65451b --- /dev/null +++ b/packages/types/src/shell/type/page-schema.ts @@ -0,0 +1,9 @@ +import { IPublicTypeContainerSchema } from './'; + +/** + * 页面容器 + * @see https://lowcode-engine.cn/lowcode + */ +export interface IPublicTypePageSchema extends IPublicTypeContainerSchema { + componentName: 'Page'; +} diff --git a/packages/types/src/shell/type/plugin-config.ts b/packages/types/src/shell/type/plugin-config.ts new file mode 100644 index 0000000000..2d841dd804 --- /dev/null +++ b/packages/types/src/shell/type/plugin-config.ts @@ -0,0 +1,5 @@ +export interface IPublicTypePluginConfig { + init(): Promise<void> | void; + destroy?(): Promise<void> | void; + exports?(): any; +} diff --git a/packages/types/src/shell/type/plugin-creater.ts b/packages/types/src/shell/type/plugin-creater.ts new file mode 100644 index 0000000000..713578752f --- /dev/null +++ b/packages/types/src/shell/type/plugin-creater.ts @@ -0,0 +1,5 @@ +import { IPublicTypePluginConfig } from './'; +import { IPublicModelPluginContext } from '../model'; + +// eslint-disable-next-line max-len +export type IPublicTypePluginCreater = (ctx: IPublicModelPluginContext, options: any) => IPublicTypePluginConfig; diff --git a/packages/types/src/shell/type/plugin-declaration-property.ts b/packages/types/src/shell/type/plugin-declaration-property.ts new file mode 100644 index 0000000000..f07b350a65 --- /dev/null +++ b/packages/types/src/shell/type/plugin-declaration-property.ts @@ -0,0 +1,21 @@ +import { IPublicTypePreferenceValueType } from './'; + +export interface IPublicTypePluginDeclarationProperty { + // shape like 'name' or 'group.name' or 'group.subGroup.name' + key: string; + // must have either one of description & markdownDescription + description: string; + // value in 'number', 'string', 'boolean' + type: string; + // default value + // NOTE! this is only used in configuration UI, won`t affect runtime + default?: IPublicTypePreferenceValueType; + // only works when type === 'string', default value false + useMultipleLineTextInput?: boolean; + // enum values, only works when type === 'string' + enum?: any[]; + // descriptions for enum values + enumDescriptions?: string[]; + // message that describing deprecation of this property + deprecationMessage?: string; +} diff --git a/packages/types/src/shell/type/plugin-declaration.ts b/packages/types/src/shell/type/plugin-declaration.ts new file mode 100644 index 0000000000..4d5e1a4e60 --- /dev/null +++ b/packages/types/src/shell/type/plugin-declaration.ts @@ -0,0 +1,11 @@ +import { IPublicTypePluginDeclarationProperty } from './'; + +/** + * declaration of plugin`s preference + * when strictPluginMode === true, only declared preference can be obtained from inside plugin. + */ +export interface IPublicTypePluginDeclaration { + // this will be displayed on configuration UI, can be plugin name + title: string; + properties: IPublicTypePluginDeclarationProperty[]; +} diff --git a/packages/types/src/shell/type/plugin-meta.ts b/packages/types/src/shell/type/plugin-meta.ts new file mode 100644 index 0000000000..bf7f6212e8 --- /dev/null +++ b/packages/types/src/shell/type/plugin-meta.ts @@ -0,0 +1,37 @@ +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; + }; + preferenceDeclaration?: IPublicTypePluginDeclaration; + + /** + * use 'common' as event prefix when eventPrefix is not set. + * strongly recommend using pluginName as eventPrefix + * + * eg. + * case 1, when eventPrefix is not specified + * event.emit('someEventName') is actually sending event with name 'common:someEventName' + * + * case 2, when eventPrefix is 'myEvent' + * 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/plugin-register-options.ts b/packages/types/src/shell/type/plugin-register-options.ts new file mode 100644 index 0000000000..7d2377bbe2 --- /dev/null +++ b/packages/types/src/shell/type/plugin-register-options.ts @@ -0,0 +1,13 @@ + +export interface IPublicTypePluginRegisterOptions { + /** + * Will enable plugin registered with auto-initialization immediately + * other than plugin-manager init all plugins at certain time. + * It is helpful when plugin register is later than plugin-manager initialization. + */ + autoInit?: boolean; + /** + * allow overriding existing plugin with same name when override === true + */ + override?: boolean; +} diff --git a/packages/types/src/shell/type/plugin.ts b/packages/types/src/shell/type/plugin.ts new file mode 100644 index 0000000000..f5d7b81e50 --- /dev/null +++ b/packages/types/src/shell/type/plugin.ts @@ -0,0 +1,7 @@ +/* eslint-disable max-len */ +import { IPublicTypePluginMeta, IPublicTypePluginCreater } from './'; + +export interface IPublicTypePlugin extends IPublicTypePluginCreater { + pluginName: string; + meta?: IPublicTypePluginMeta; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/preference-value-type.ts b/packages/types/src/shell/type/preference-value-type.ts new file mode 100644 index 0000000000..75b58824c0 --- /dev/null +++ b/packages/types/src/shell/type/preference-value-type.ts @@ -0,0 +1,2 @@ + +export type IPublicTypePreferenceValueType = string | number | boolean; diff --git a/packages/types/src/shell/type/project-schema.ts b/packages/types/src/shell/type/project-schema.ts new file mode 100644 index 0000000000..271841bfb3 --- /dev/null +++ b/packages/types/src/shell/type/project-schema.ts @@ -0,0 +1,68 @@ +import { InterpretDataSource as DataSource } from '@alilc/lowcode-datasource-types'; +import { IPublicTypeJSONObject, IPublicTypeRootSchema, IPublicTypeI18nMap, IPublicTypeAppConfig, IPublicTypeComponentsMap, IPublicTypeJSExpression, IPublicTypeJSFunction, IPublicTypeNpmInfo } from './'; + +export interface IPublicTypeInternalUtils { + name: string; + type: 'function'; + content: IPublicTypeJSFunction | IPublicTypeJSExpression; +} + +export interface IPublicTypeExternalUtils { + name: string; + type: 'npm' | 'tnpm'; + content: IPublicTypeNpmInfo; +} + +export type IPublicTypeUtilItem = IPublicTypeInternalUtils | IPublicTypeExternalUtils; +export type IPublicTypeUtilsMap = IPublicTypeUtilItem[]; +/** + * 应用描述 + */ + +export interface IPublicTypeProjectSchema<T = IPublicTypeRootSchema> { + id?: string; + /** + * 当前应用协议版本号 + */ + version: string; + /** + * 当前应用所有组件映射关系 + */ + componentsMap: IPublicTypeComponentsMap; + /** + * 描述应用所有页面、低代码组件的组件树 + * 低代码业务组件树描述 + * 是长度固定为 1 的数组,即数组内仅包含根容器的描述(低代码业务组件容器类型) + */ + componentsTree: T[]; + /** + * 国际化语料 + */ + i18n?: IPublicTypeI18nMap; + /** + * 应用范围内的全局自定义函数或第三方工具类扩展 + */ + utils?: IPublicTypeUtilsMap; + /** + * 应用范围内的全局常量 + */ + constants?: IPublicTypeJSONObject; + /** + * 应用范围内的全局样式 + */ + css?: string; + /** + * 当前应用的公共数据源 + */ + dataSource?: DataSource; + /** + * 当前应用配置信息 + * + * TODO: 需要在后续版本中移除 `Record<string, unknown>` 类型签名 + */ + config?: IPublicTypeAppConfig & Record<string, unknown>; + /** + * 当前应用元数据信息 + */ + meta?: Record<string, any>; +} diff --git a/packages/types/src/shell/type/prop-change-options.ts b/packages/types/src/shell/type/prop-change-options.ts new file mode 100644 index 0000000000..b515aec537 --- /dev/null +++ b/packages/types/src/shell/type/prop-change-options.ts @@ -0,0 +1,14 @@ +import { + IPublicModelNode, + IPublicModelProp, +} from '../model'; + +export interface IPublicTypePropChangeOptions< + Node = IPublicModelNode +> { + key?: string | number; + prop?: IPublicModelProp; + node: Node; + newValue: any; + oldValue: any; +} diff --git a/packages/types/src/shell/type/prop-config.ts b/packages/types/src/shell/type/prop-config.ts new file mode 100644 index 0000000000..e7635659cb --- /dev/null +++ b/packages/types/src/shell/type/prop-config.ts @@ -0,0 +1,27 @@ +import { IPublicTypePropType } from './'; + +/** + * 组件属性信息 + */ +export interface IPublicTypePropConfig { + /** + * 属性名称 + */ + name: string; + /** + * 属性类型 + */ + propType: IPublicTypePropType; + /** + * 属性描述 + */ + description?: string; + /** + * 属性默认值 + */ + defaultValue?: any; + /** + * @deprecated 已被弃用 + */ + setter?: any; +} diff --git a/packages/types/src/shell/type/prop-types.ts b/packages/types/src/shell/type/prop-types.ts new file mode 100644 index 0000000000..22d84c86fc --- /dev/null +++ b/packages/types/src/shell/type/prop-types.ts @@ -0,0 +1,48 @@ +/* eslint-disable max-len */ +import { IPublicTypePropConfig } from './'; + +export type IPublicTypePropType = IPublicTypeBasicType | IPublicTypeRequiredType | IPublicTypeComplexType; +export type IPublicTypeBasicType = 'array' | 'bool' | 'func' | 'number' | 'object' | 'string' | 'node' | 'element' | 'any'; +export type IPublicTypeComplexType = IPublicTypeOneOf | IPublicTypeOneOfType | IPublicTypeArrayOf | IPublicTypeObjectOf | IPublicTypeShape | IPublicTypeExact | IPublicTypeInstanceOf; + +export interface IPublicTypeRequiredType { + type: IPublicTypeBasicType; + isRequired?: boolean; +} + +export interface IPublicTypeOneOf { + type: 'oneOf'; + value: string[]; + isRequired?: boolean; +} +export interface IPublicTypeOneOfType { + type: 'oneOfType'; + value: IPublicTypePropType[]; + isRequired?: boolean; +} +export interface IPublicTypeArrayOf { + type: 'arrayOf'; + value: IPublicTypePropType; + isRequired?: boolean; +} +export interface IPublicTypeObjectOf { + type: 'objectOf'; + value: IPublicTypePropType; + isRequired?: boolean; +} +export interface IPublicTypeShape { + type: 'shape'; + value: IPublicTypePropConfig[]; + isRequired?: boolean; +} +export interface IPublicTypeExact { + type: 'exact'; + value: IPublicTypePropConfig[]; + isRequired?: boolean; +} + +export interface IPublicTypeInstanceOf { + type: 'instanceOf'; + value: IPublicTypePropConfig; + isRequired?: boolean; +} diff --git a/packages/types/src/shell/type/props-list.ts b/packages/types/src/shell/type/props-list.ts new file mode 100644 index 0000000000..801c088b64 --- /dev/null +++ b/packages/types/src/shell/type/props-list.ts @@ -0,0 +1,7 @@ +import { IPublicTypeCompositeValue } from './'; + +export type IPublicTypePropsList = Array<{ + spread?: boolean; + name?: string; + value: IPublicTypeCompositeValue; +}>; diff --git a/packages/types/src/shell/type/props-map.ts b/packages/types/src/shell/type/props-map.ts new file mode 100644 index 0000000000..1b93f46252 --- /dev/null +++ b/packages/types/src/shell/type/props-map.ts @@ -0,0 +1,3 @@ +import { IPublicTypeCompositeObject, IPublicTypeNodeData } from './'; + +export type IPublicTypePropsMap = IPublicTypeCompositeObject<IPublicTypeNodeData | IPublicTypeNodeData[]>; diff --git a/packages/types/src/shell/type/props-transducer.ts b/packages/types/src/shell/type/props-transducer.ts new file mode 100644 index 0000000000..b98ec36a6f --- /dev/null +++ b/packages/types/src/shell/type/props-transducer.ts @@ -0,0 +1,11 @@ +import { IPublicEnumTransformStage } from '../enum'; +import { IPublicModelNode } from '../model'; +import { IPublicTypeCompositeObject } from './'; + +export type IPublicTypePropsTransducer = ( + props: IPublicTypeCompositeObject, + node: IPublicModelNode, + ctx?: { + stage: IPublicEnumTransformStage; + }, +) => IPublicTypeCompositeObject; diff --git a/packages/types/src/shell/type/reference.ts b/packages/types/src/shell/type/reference.ts new file mode 100644 index 0000000000..34de153d99 --- /dev/null +++ b/packages/types/src/shell/type/reference.ts @@ -0,0 +1,35 @@ +import { EitherOr } from '../../utils'; + +/** + * 资源引用信息,Npm 的升级版本, + */ +export type IPublicTypeReference = EitherOr<{ + /** + * 引用资源的 id 标识 + */ + id: string; + /** + * 引用资源的包名 + */ + package: string; + /** + * 引用资源的导出对象中的属性值名称 + */ + exportName: string; + /** + * 引用 exportName 上的子对象 + */ + subName: string; + /** + * 引用的资源主入口 + */ + main?: string; + /** + * 是否从引用资源的导出对象中获取属性值 + */ + destructuring?: boolean; + /** + * 资源版本号 + */ + version: string; +}, 'package', 'id'>; diff --git a/packages/types/src/shell/type/registered-setter.ts b/packages/types/src/shell/type/registered-setter.ts new file mode 100644 index 0000000000..55a90465a8 --- /dev/null +++ b/packages/types/src/shell/type/registered-setter.ts @@ -0,0 +1,21 @@ +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: IPublicModelSettingField) => boolean; + + /** + * for MixedSetter to manual change to this setter + */ + initialValue?: any | ((field: IPublicModelSettingField) => any); + recommend?: boolean; + // 标识是否为动态 setter,默认为 true + isDynamic?: boolean; +} diff --git a/packages/types/src/shell/type/remote-component-description.ts b/packages/types/src/shell/type/remote-component-description.ts new file mode 100644 index 0000000000..2337203657 --- /dev/null +++ b/packages/types/src/shell/type/remote-component-description.ts @@ -0,0 +1,30 @@ +import { Asset } from '../../assets'; +import { IPublicTypeComponentMetadata, IPublicTypeReference } from './'; + +/** + * 远程物料描述 + */ +export interface IPublicTypeRemoteComponentDescription extends IPublicTypeComponentMetadata { + + /** + * 组件描述导出名字,可以通过 window[exportName] 获取到组件描述的 Object 内容; + */ + exportName?: string; + + /** + * 组件描述的资源链接; + */ + url?: Asset; + + /** + * 组件 (库) 的 npm 信息; + */ + package?: { + npm?: string; + }; + + /** + * 替代 npm 字段的升级版本 + */ + reference?: IPublicTypeReference; +} diff --git a/packages/types/src/shell/type/resource-list.ts b/packages/types/src/shell/type/resource-list.ts new file mode 100644 index 0000000000..1d7c34232a --- /dev/null +++ b/packages/types/src/shell/type/resource-list.ts @@ -0,0 +1,37 @@ +import { ReactElement } from 'react'; + +export interface IPublicResourceData { + + /** 资源名字 */ + resourceName: string; + + /** 资源扩展配置 */ + config?: { + [key: string]: any; + }; + + /** 资源标题 */ + title?: string; + + /** 资源 Id */ + id?: string; + + /** 分类 */ + category?: string; + + /** 资源视图 */ + viewName?: string; + + /** 资源 icon */ + icon?: ReactElement; + + /** 资源其他配置,资源初始化时的第二个参数 */ + options: { + [key: string]: any; + }; + + /** 资源子元素 */ + children?: IPublicResourceData[]; +} + +export type IPublicResourceList = IPublicResourceData[]; \ No newline at end of file diff --git a/packages/types/src/shell/type/resource-type-config.ts b/packages/types/src/shell/type/resource-type-config.ts new file mode 100644 index 0000000000..01b49aa2bd --- /dev/null +++ b/packages/types/src/shell/type/resource-type-config.ts @@ -0,0 +1,41 @@ +import React from 'react'; +import { IPublicTypeEditorView } from './editor-view'; + +export interface IPublicResourceTypeConfig { + + /** 资源描述 */ + description?: string; + + /** 资源 icon 标识 */ + icon?: React.ReactElement | React.FunctionComponent | React.ComponentClass; + + /** + * 默认视图类型 + * @deprecated + */ + defaultViewType?: string; + + /** 默认视图类型 */ + defaultViewName: string; + + /** 资源视图 */ + editorViews: IPublicTypeEditorView[]; + + init?: () => void; + + /** save 钩子 */ + save?: (schema: { + [viewName: string]: any; + }) => Promise<void>; + + /** import 钩子 */ + import?: (schema: any) => Promise<{ + [viewName: string]: any; + }>; + + /** 默认标题 */ + defaultTitle?: string; + + /** resourceType 类型为 'webview' 时渲染的地址 */ + url?: () => Promise<string>; +} diff --git a/packages/types/src/shell/type/resource-type.ts b/packages/types/src/shell/type/resource-type.ts new file mode 100644 index 0000000000..7d64a4463d --- /dev/null +++ b/packages/types/src/shell/type/resource-type.ts @@ -0,0 +1,10 @@ +import { IPublicModelPluginContext } from '../model'; +import { IPublicResourceTypeConfig } from './resource-type-config'; + +export interface IPublicTypeResourceType { + resourceName: string; + + resourceType: 'editor' | 'webview' | string; + + (ctx: IPublicModelPluginContext, options: Object): IPublicResourceTypeConfig; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/root-schema.ts b/packages/types/src/shell/type/root-schema.ts new file mode 100644 index 0000000000..16f3bf94ec --- /dev/null +++ b/packages/types/src/shell/type/root-schema.ts @@ -0,0 +1,7 @@ +import { IPublicTypePageSchema, IPublicTypeComponentSchema, IPublicTypeBlockSchema } from './'; + +/** + * @todo + */ +// eslint-disable-next-line max-len +export type IPublicTypeRootSchema = IPublicTypePageSchema | IPublicTypeComponentSchema | IPublicTypeBlockSchema; diff --git a/packages/types/src/shell/type/scrollable.ts b/packages/types/src/shell/type/scrollable.ts new file mode 100644 index 0000000000..b308637e0c --- /dev/null +++ b/packages/types/src/shell/type/scrollable.ts @@ -0,0 +1,7 @@ +import { IPublicModelScrollTarget } from '../model'; + +export interface IPublicTypeScrollable { + scrollTarget?: IPublicModelScrollTarget | Element; + bounds?: DOMRect | null; + scale?: number; +} diff --git a/packages/types/src/shell/type/set-value-options.ts b/packages/types/src/shell/type/set-value-options.ts new file mode 100644 index 0000000000..814f458459 --- /dev/null +++ b/packages/types/src/shell/type/set-value-options.ts @@ -0,0 +1,7 @@ +import { IPublicEnumPropValueChangedType } from '../enum'; + +export interface IPublicTypeSetValueOptions { + disableMutator?: boolean; + type?: IPublicEnumPropValueChangedType; + fromSetHotValue?: boolean; +} diff --git a/packages/types/src/shell/type/setter-config.ts b/packages/types/src/shell/type/setter-config.ts new file mode 100644 index 0000000000..c0f93679ee --- /dev/null +++ b/packages/types/src/shell/type/setter-config.ts @@ -0,0 +1,64 @@ +import { IPublicTypeCustomView, IPublicTypeCompositeValue, IPublicTypeTitleContent, IPublicModelSettingField } from '..'; +import { IPublicTypeDynamicProps } from './dynamic-props'; + +/** + * 设置器配置 + */ +export interface IPublicTypeSetterConfig { + + // if *string* passed must be a registered Setter Name + /** + * 配置设置器用哪一个 setter + */ + componentName: string | IPublicTypeCustomView; + + /** + * 传递给 setter 的属性 + * + * the props pass to Setter Component + */ + props?: Record<string, unknown> | IPublicTypeDynamicProps; + + /** + * @deprecated + */ + children?: any; + + /** + * 是否必填? + * + * ArraySetter 里有个快捷预览,可以在不打开面板的情况下直接编辑 + */ + isRequired?: boolean; + + /** + * Setter 的初始值 + * + * @todo initialValue 可能要和 defaultValue 二选一 + */ + initialValue?: any | ((target: IPublicModelSettingField) => any); + + defaultValue?: any; + + // for MixedSetter + /** + * 给 MixedSetter 时切换 Setter 展示用的 + */ + title?: IPublicTypeTitleContent; + + // for MixedSetter check this is available + /** + * 给 MixedSetter 用于判断优先选中哪个 + */ + condition?: (target: IPublicModelSettingField) => boolean; + + /** + * 给 MixedSetter,切换值时声明类型 + * + * @todo 物料协议推进 + */ + valueType?: IPublicTypeCompositeValue[]; + + // 标识是否为动态 setter,默认为 true + isDynamic?: boolean; +} diff --git a/packages/types/src/shell/type/setter-type.ts b/packages/types/src/shell/type/setter-type.ts new file mode 100644 index 0000000000..92cb118caf --- /dev/null +++ b/packages/types/src/shell/type/setter-type.ts @@ -0,0 +1,6 @@ +import { IPublicTypeCustomView, IPublicTypeSetterConfig } from './'; + +// if *string* passed must be a registered Setter Name, future support blockSchema + +// eslint-disable-next-line max-len +export type IPublicTypeSetterType = IPublicTypeSetterConfig | IPublicTypeSetterConfig[] | string | IPublicTypeCustomView; diff --git a/packages/types/src/shell/type/simulator-renderer.ts b/packages/types/src/shell/type/simulator-renderer.ts new file mode 100644 index 0000000000..14aa16ab88 --- /dev/null +++ b/packages/types/src/shell/type/simulator-renderer.ts @@ -0,0 +1,32 @@ +import { Asset } from '../../assets'; +import { + IPublicTypeNodeInstance, + IPublicTypeProjectSchema, + IPublicTypeComponentSchema, +} from './'; + +export interface IPublicTypeSimulatorRenderer<Component, ComponentInstance> { + readonly isSimulatorRenderer: true; + autoRepaintNode?: boolean; + components: Record<string, Component>; + rerender: () => void; + createComponent( + schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema>, + ): Component | null; + getComponent(componentName: string): Component; + getClosestNodeInstance( + from: ComponentInstance, + nodeId?: string, + ): IPublicTypeNodeInstance<ComponentInstance> | null; + findDOMNodes(instance: ComponentInstance): Array<Element | Text> | null; + getClientRects(element: Element | Text): DOMRect[]; + setNativeSelection(enableFlag: boolean): void; + setDraggingState(state: boolean): void; + setCopyState(state: boolean): void; + loadAsyncLibrary(asyncMap: { [index: string]: any }): void; + clearState(): void; + stopAutoRepaintNode(): void; + enableAutoRepaintNode(): void; + run(): void; + load(asset: Asset): Promise<any>; +} diff --git a/packages/types/src/shell/type/slot-schema.ts b/packages/types/src/shell/type/slot-schema.ts new file mode 100644 index 0000000000..8928a98247 --- /dev/null +++ b/packages/types/src/shell/type/slot-schema.ts @@ -0,0 +1,18 @@ +import { IPublicTypeNodeData } from './node-data'; +import { IPublicTypeNodeSchema } from './node-schema'; + +/** + * Slot schema 描述 + */ +export interface IPublicTypeSlotSchema extends IPublicTypeNodeSchema { + componentName: 'Slot'; + name?: string; + title?: string; + params?: string[]; + props?: { + slotTitle?: string; + slotName?: string; + slotParams?: string[]; + }; + children?: IPublicTypeNodeData[] | IPublicTypeNodeData; +} diff --git a/packages/types/src/shell/type/snippet.ts b/packages/types/src/shell/type/snippet.ts new file mode 100644 index 0000000000..f844777fb8 --- /dev/null +++ b/packages/types/src/shell/type/snippet.ts @@ -0,0 +1,27 @@ +import { IPublicTypeNodeSchema } from './'; + +/** + * 可用片段 + * + * 内容为组件不同状态下的低代码 schema (可以有多个),用户从组件面板拖入组件到设计器时会向页面 schema 中插入 snippets 中定义的组件低代码 schema + */ +export interface IPublicTypeSnippet { + /** + * 组件分类 title + */ + title?: string; + /** + * snippet 截图 + */ + screenshot?: string; + /** + * snippet 打标 + * + * @deprecated 暂未使用 + */ + label?: string; + /** + * 待插入的 schema + */ + schema?: IPublicTypeNodeSchema; +} diff --git a/packages/types/src/shell/type/tip-config.ts b/packages/types/src/shell/type/tip-config.ts new file mode 100644 index 0000000000..f8b271c909 --- /dev/null +++ b/packages/types/src/shell/type/tip-config.ts @@ -0,0 +1,21 @@ +import { IPublicTypeI18nData } from '..'; +import { ReactNode } from 'react'; + +export interface IPublicTypeTipConfig { + + /** + * className + */ + className?: string; + + /** + * tip 的内容 + */ + children?: IPublicTypeI18nData | ReactNode; + theme?: string; + + /** + * tip 的方向 + */ + direction?: 'top' | 'bottom' | 'left' | 'right'; +} diff --git a/packages/types/src/shell/type/tip-content.ts b/packages/types/src/shell/type/tip-content.ts new file mode 100644 index 0000000000..340d404aba --- /dev/null +++ b/packages/types/src/shell/type/tip-content.ts @@ -0,0 +1,5 @@ +import { IPublicTypeI18nData } from '..'; +import { ReactNode } from 'react'; +import { IPublicTypeTipConfig } from './tip-config'; + +export type TipContent = string | IPublicTypeI18nData | ReactNode | IPublicTypeTipConfig; diff --git a/packages/types/src/shell/type/title-config.ts b/packages/types/src/shell/type/title-config.ts new file mode 100644 index 0000000000..f8de287599 --- /dev/null +++ b/packages/types/src/shell/type/title-config.ts @@ -0,0 +1,53 @@ +import { ReactNode } from 'react'; +import { IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeTitleContent, TipContent } from './'; + +export interface IPublicTypeTitleProps { + + /** + * 标题内容 + */ + title: IPublicTypeTitleContent; + + /** + * className + */ + className?: string; + + /** + * 点击事件 + */ + onClick?: () => void; + match?: boolean; + keywords?: string; +} + +/** + * 描述 props 的 setter title + */ +export interface IPublicTypeTitleConfig { + + /** + * 文字描述 + */ + label?: IPublicTypeI18nData | ReactNode; + + /** + * hover 后的展现内容 + */ + tip?: TipContent; + + /** + * 文档链接,暂未实现 + */ + docUrl?: string; + + /** + * 图标 + */ + icon?: IPublicTypeIconType; + + /** + * CSS 类 + */ + className?: string; +} diff --git a/packages/types/src/shell/type/title-content.ts b/packages/types/src/shell/type/title-content.ts new file mode 100644 index 0000000000..b17a476a5c --- /dev/null +++ b/packages/types/src/shell/type/title-content.ts @@ -0,0 +1,5 @@ +import { ReactElement, ReactNode } from 'react'; +import { IPublicTypeI18nData, IPublicTypeTitleConfig } from './'; + +// eslint-disable-next-line max-len +export type IPublicTypeTitleContent = string | IPublicTypeI18nData | ReactElement | ReactNode | IPublicTypeTitleConfig; \ No newline at end of file diff --git a/packages/types/src/shell/type/transformed-component-metadata.ts b/packages/types/src/shell/type/transformed-component-metadata.ts new file mode 100644 index 0000000000..6baa21c18b --- /dev/null +++ b/packages/types/src/shell/type/transformed-component-metadata.ts @@ -0,0 +1,8 @@ +import { IPublicTypeComponentMetadata, IPublicTypeFieldConfig, IPublicTypeConfigure } from './'; + +/** + * @todo 待补充文档 + */ +export interface IPublicTypeTransformedComponentMetadata extends IPublicTypeComponentMetadata { + configure: IPublicTypeConfigure & { combined?: IPublicTypeFieldConfig[] }; +} diff --git a/packages/types/src/shell/type/value-type.ts b/packages/types/src/shell/type/value-type.ts new file mode 100644 index 0000000000..16fb789a26 --- /dev/null +++ b/packages/types/src/shell/type/value-type.ts @@ -0,0 +1,136 @@ +import { IPublicTypeNodeData, IPublicTypeCompositeValue, IPublicTypeNodeSchema } from './'; + +/** + * 变量表达式 + * + * 表达式内通过 this 对象获取上下文 + */ +export interface IPublicTypeJSExpression { + type: 'JSExpression'; + + /** + * 表达式字符串 + */ + value: string; + + /** + * 模拟值 + * + * @todo 待标准描述 + */ + mock?: any; + + /** + * 源码 + * + * @todo 待标准描述 + */ + compiled?: string; +} + +/** + * 事件函数类型 + * @see https://lowcode-engine.cn/lowcode + * + * 保留与原组件属性、生命周期 ( React / 小程序) 一致的输入参数,并给所有事件函数 binding 统一一致的上下文(当前组件所在容器结构的 this 对象) + */ +export interface IPublicTypeJSFunction { + type: 'JSFunction'; + + /** + * 函数定义,或直接函数表达式 + */ + value: string; + + /** + * 源码 + * + * @todo 待标准描述 + */ + compiled?: string; + + /** + * 模拟值 + * + * @todo 待标准描述 + */ + mock?: any; + + /** + * 额外扩展属性,如 extType、events + * + * @todo 待标准描述 + */ + [key: string]: any; +} + +/** + * Slot 函数类型 + * + * 通常用于描述组件的某一个属性为 ReactNode 或 Function return ReactNode 的场景。 + */ +export interface IPublicTypeJSSlot { + + /** + * type + */ + type: 'JSSlot'; + + /** + * @todo 待标准描述 + */ + title?: string; + + /** + * @todo 待标准描述 + */ + id?: string; + + /** + * 组件的某一个属性为 Function return ReactNode 时,函数的入参 + * + * 其子节点可以通过 this[参数名] 来获取对应的参数。 + */ + params?: string[]; + + /** + * 具体的值。 + */ + value?: IPublicTypeNodeData[] | IPublicTypeNodeData; + + /** + * @todo 待标准描述 + */ + name?: string; +} + +/** + * @deprecated + * + * @todo 待文档描述 + */ +export interface IPublicTypeJSBlock { + type: 'JSBlock'; + value: IPublicTypeNodeSchema; +} + +/** + * JSON 基本类型 + */ +export type IPublicTypeJSONValue = + | boolean + | string + | number + | null + | undefined + | IPublicTypeJSONArray + | IPublicTypeJSONObject; +export type IPublicTypeJSONArray = IPublicTypeJSONValue[]; +export interface IPublicTypeJSONObject { + [key: string]: IPublicTypeJSONValue; +} + +export type IPublicTypeCompositeArray = IPublicTypeCompositeValue[]; +export interface IPublicTypeCompositeObject<T = IPublicTypeCompositeValue> { + [key: string]: IPublicTypeCompositeValue | T; +} \ No newline at end of file diff --git a/packages/types/src/shell/type/widget-base-config.ts b/packages/types/src/shell/type/widget-base-config.ts new file mode 100644 index 0000000000..2764ce1927 --- /dev/null +++ b/packages/types/src/shell/type/widget-base-config.ts @@ -0,0 +1,99 @@ +import { ReactElement, ComponentType } from 'react'; +import { IPublicTypeI18nData, IPublicTypeIconType, IPublicTypeTitleContent, IPublicTypeWidgetConfigArea, TipContent } from './'; + +export type IPublicTypeHelpTipConfig = string | { url?: string; content?: string | ReactElement }; + +export interface IPublicTypePanelConfigProps extends IPublicTypePanelDockPanelProps { + title?: IPublicTypeTitleContent; + icon?: any; // 冗余字段 + description?: string | IPublicTypeI18nData; + help?: IPublicTypeHelpTipConfig; // 显示问号帮助 + hiddenWhenInit?: boolean; // when this is true, by default will be hidden + condition?: (widget: any) => any; + onInit?: (widget: any) => any; + onDestroy?: () => any; + shortcut?: string; // 只有在特定位置,可触发 toggle show + enableDrag?: boolean; // 是否开启通过 drag 调整 宽度 + keepVisibleWhileDragging?: boolean; // 是否在该 panel 范围内拖拽时保持 visible 状态 +} + +export interface IPublicTypePanelConfig extends IPublicTypeWidgetBaseConfig { + type: 'Panel'; + content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[]; // as children + props?: IPublicTypePanelConfigProps; +} + +export interface IPublicTypeWidgetBaseConfig { + [extra: string]: any; + type: string; + name: string; + + /** + * 停靠位置: + * - 当 type 为 'Panel' 时自动为 'leftFloatArea'; + * - 当 type 为 'Widget' 时自动为 'mainArea'; + * - 其他时候自动为 'leftArea'; + */ + area?: IPublicTypeWidgetConfigArea; + props?: Record<string, any>; + content?: string | ReactElement | ComponentType<any> | IPublicTypePanelConfig[]; + contentProps?: Record<string, any>; + + /** + * 优先级,值越小,优先级越高,优先级高的会排在前面 + */ + index?: number; +} + +export interface IPublicTypePanelDockConfig extends IPublicTypeWidgetBaseConfig { + type: 'PanelDock'; + + panelProps?: IPublicTypePanelDockPanelProps; + + props?: IPublicTypePanelDockProps; + + /** 面板 name, 当没有 props.title 时, 会使用 name 作为标题 */ + name: string; +} + +export interface IPublicTypePanelDockProps { + [key: string]: any; + + size?: 'small' | 'medium' | 'large'; + + className?: string; + + /** 详细描述,hover 时在标题上方显示的 tips 内容 */ + description?: TipContent; + + onClick?: () => void; + + /** + * 面板标题前的 icon + */ + icon?: IPublicTypeIconType; + + /** + * 面板标题 + */ + title?: IPublicTypeTitleContent; +} + +export interface IPublicTypePanelDockPanelProps { + [key: string]: any; + + /** 是否隐藏面板顶部条 */ + hideTitleBar?: boolean; + + width?: number; + + height?: number; + + maxWidth?: number; + + maxHeight?: number; + + area?: IPublicTypeWidgetConfigArea; +} + +export type IPublicTypeSkeletonConfig = IPublicTypePanelDockConfig | IPublicTypeWidgetBaseConfig; \ No newline at end of file diff --git a/packages/types/src/shell/type/widget-config-area.ts b/packages/types/src/shell/type/widget-config-area.ts new file mode 100644 index 0000000000..41e71baa26 --- /dev/null +++ b/packages/types/src/shell/type/widget-config-area.ts @@ -0,0 +1,9 @@ +/** + * 所有可能的停靠位置 + */ +export type IPublicTypeWidgetConfigArea = 'leftArea' | 'left' | 'rightArea' | + 'right' | 'topArea' | 'subTopArea' | 'top' | + 'toolbar' | 'mainArea' | 'main' | + 'center' | 'centerArea' | 'bottomArea' | + 'bottom' | 'leftFixedArea' | + 'leftFloatArea' | 'stages'; diff --git a/packages/types/src/tip.ts b/packages/types/src/tip.ts deleted file mode 100644 index 6206aef6be..0000000000 --- a/packages/types/src/tip.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { I18nData } from './i18n'; -import { ReactNode } from 'react'; - -export interface TipConfig { - className?: string; - children?: I18nData | ReactNode; - theme?: string; - direction?: 'top' | 'bottom' | 'left' | 'right'; -} - -export type TipContent = string | I18nData | ReactNode | TipConfig; diff --git a/packages/types/src/title.ts b/packages/types/src/title.ts deleted file mode 100644 index a4ac366144..0000000000 --- a/packages/types/src/title.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ReactElement, ReactNode } from 'react'; -import { I18nData, isI18nData } from './i18n'; -import { TipContent } from './tip'; -import { IconType } from './icon'; - -/** - * 描述 props 的 setter title - */ -export interface TitleConfig { - /** - * 文字描述 - */ - label?: I18nData | ReactNode; - /** - * hover 后的展现内容 - */ - tip?: TipContent; - /** - * 文档链接,暂未实现 - */ - docUrl?: string; - /** - * 图标 - */ - icon?: IconType; - /** - * CSS 类 - */ - className?: string; -} - -export type TitleContent = string | I18nData | ReactElement | TitleConfig; - -function isPlainObject(value: any): value is Record<string, unknown> { - if (typeof value !== 'object') { - return false; - } - const proto = Object.getPrototypeOf(value); - return proto === Object.prototype || proto === null || Object.getPrototypeOf(proto) === null; -} - -export function isTitleConfig(obj: any): obj is TitleConfig { - return isPlainObject(obj) && !isI18nData(obj); -} diff --git a/packages/types/src/transform-stage.ts b/packages/types/src/transform-stage.ts deleted file mode 100644 index b16cb0ff4d..0000000000 --- a/packages/types/src/transform-stage.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum TransformStage { - Render = 'render', - Serilize = 'serilize', - Save = 'save', - Clone = 'clone', - Init = 'init', - Upgrade = 'upgrade', -} diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index b8ac406efd..2914597a71 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -1,17 +1,34 @@ -import { NpmInfo } from './npm'; -import { JSExpression, JSFunction } from './value-type'; -export type InternalUtils = { - name: string; - type: 'function'; - content: JSFunction | JSExpression; -}; +type FilterOptional<T> = Pick< + T, + Exclude< + { + [K in keyof T]: T extends Record<K, T[K]> ? K : never; + }[keyof T], + undefined + > +>; + +type FilterNotOptional<T> = Pick< + T, + Exclude< + { + [K in keyof T]: T extends Record<K, T[K]> ? never : K; + }[keyof T], + undefined + > +>; + +type PartialEither<T, K extends keyof any> = { [P in Exclude<keyof FilterOptional<T>, K>]-?: T[P] } & + { [P in Exclude<keyof FilterNotOptional<T>, K>]?: T[P] } & + { [P in Extract<keyof T, K>]?: undefined }; -export type ExternalUtils = { - name: string; - type: 'npm' | 'tnpm'; - content: NpmInfo; +type Object = { + [name: string]: any; }; -export type UtilItem = InternalUtils | ExternalUtils; -export type UtilsMap = UtilItem[]; +export type EitherOr<O extends Object, L extends string, R extends string> = + ( + PartialEither<Pick<O, L | R>, L> | + PartialEither<Pick<O, L | R>, R> + ) & Omit<O, L | R>; diff --git a/packages/types/src/value-type.ts b/packages/types/src/value-type.ts deleted file mode 100644 index 933e9a6980..0000000000 --- a/packages/types/src/value-type.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { NodeSchema, NodeData } from './schema'; - -/** - * 变量表达式 - * - * 表达式内通过 this 对象获取上下文 - */ -export interface JSExpression { - type: 'JSExpression'; - /** - * 表达式字符串 - */ - value: string; - /** - * 模拟值 - * - * @todo 待标准描述 - */ - mock?: any; - /** - * 源码 - * - * @todo 待标准描述 - */ - compiled?: string; -} - -/** - * 事件函数类型 - * @see https://lowcode-engine.cn/lowcode - * - * 保留与原组件属性、生命周期( React / 小程序)一致的输入参数,并给所有事件函数 binding 统一一致的上下文(当前组件所在容器结构的 this 对象) - */ -export interface JSFunction { - type: 'JSFunction'; - /** - * 函数定义,或直接函数表达式 - */ - value: string; - - /** - * 源码 - * - * @todo 待标准描述 - */ - compiled?: string; - - /** - * 模拟值 - * - * @todo 待标准描述 - */ - mock?: any; - - /** - * 额外扩展属性,如 extType、events - * - * @todo 待标准描述 - */ - [key: string]: any; -} - -/** - * Slot 函数类型 - * - * 通常用于描述组件的某一个属性为 ReactNode 或 Function return ReactNode 的场景。 - */ -export interface JSSlot { - type: 'JSSlot'; - /** - * @todo 待标准描述 - */ - title?: string; - /** - * 组件的某一个属性为 Function return ReactNode 时,函数的入参 - * - * 其子节点可以通过this[参数名] 来获取对应的参数。 - */ - params?: string[]; - /** - * 具体的值。 - */ - value?: NodeData[] | NodeData; - /** - * @todo 待标准描述 - */ - name?: string; -} - -/** - * @deprecated - * - * @todo 待文档描述 - */ -export interface JSBlock { - type: 'JSBlock'; - value: NodeSchema; -} - -/** - * JSON 基本类型 - */ -export type JSONValue = - | boolean - | string - | number - | null - | undefined - | JSONArray - | JSONObject; -export type JSONArray = JSONValue[]; -export interface JSONObject { - [key: string]: JSONValue; -} - -/** - * 复合类型 - */ -export type CompositeValue = - | JSONValue - | JSExpression - | JSFunction - | JSSlot - | CompositeArray - | CompositeObject; -export type CompositeArray = CompositeValue[]; -export interface CompositeObject { - [key: string]: CompositeValue; -} - -export function isJSExpression(data: any): data is JSExpression { - return data && data.type === 'JSExpression'; -} - -export function isJSFunction(x: any): x is JSFunction { - return typeof x === 'object' && x && x.type === 'JSFunction'; -} - -export function isJSSlot(data: any): data is JSSlot { - return data && data.type === 'JSSlot'; -} - -export function isJSBlock(data: any): data is JSBlock { - return data && data.type === 'JSBlock'; -} diff --git a/packages/utils/build.json b/packages/utils/build.json index bd5cf18dde..3e92600554 100644 --- a/packages/utils/build.json +++ b/packages/utils/build.json @@ -1,5 +1,5 @@ { "plugins": [ - "build-plugin-component" + "@alilc/build-plugin-lce" ] } diff --git a/packages/utils/build.test.json b/packages/utils/build.test.json index dcdc891e93..9cc30d7463 100644 --- a/packages/utils/build.test.json +++ b/packages/utils/build.test.json @@ -1,6 +1,6 @@ { "plugins": [ - "build-plugin-component", + "@alilc/build-plugin-lce", "@alilc/lowcode-test-mate/plugin/index.ts" ] } diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index 0e05687d78..0631fa00c9 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -1,9 +1,21 @@ -module.exports = { +const fs = require('fs'); +const { join } = require('path'); +const pkgNames = fs.readdirSync(join('..')).filter(pkgName => !pkgName.startsWith('.')); + +const jestConfig = { moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], - collectCoverage: true, + collectCoverage: false, collectCoverageFrom: [ - 'src/**/*.{ts,tsx}', + 'src/**/*.ts', + '!src/**/*.d.ts', '!**/node_modules/**', '!**/vendor/**', ], + setupFilesAfterEnv: ['./jest.setup.js'], }; + +// 只对本仓库内的 pkg 做 mapping +jestConfig.moduleNameMapper = {}; +jestConfig.moduleNameMapper[`^@alilc/lowcode\\-(${pkgNames.join('|')})$`] = '<rootDir>/../$1/src'; + +module.exports = jestConfig; \ No newline at end of file diff --git a/packages/utils/jest.setup.js b/packages/utils/jest.setup.js new file mode 100644 index 0000000000..7b0828bfa8 --- /dev/null +++ b/packages/utils/jest.setup.js @@ -0,0 +1 @@ +import '@testing-library/jest-dom'; diff --git a/packages/utils/package.json b/packages/utils/package.json index 7e2841779b..60605d81e7 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@alilc/lowcode-utils", - "version": "1.0.15", + "version": "1.3.2", "description": "Utils for Ali lowCode engine", "files": [ "lib", @@ -9,21 +9,24 @@ "main": "lib/index.js", "module": "es/index.js", "scripts": { - "test": "build-scripts test --config build.test.json", - "build": "build-scripts build --skip-demo" + "test": "build-scripts test --config build.test.json --jest-coverage", + "build": "build-scripts build" }, "dependencies": { "@alifd/next": "^1.19.16", - "@alilc/lowcode-types": "1.0.15", + "@alilc/lowcode-types": "1.3.2", "lodash": "^4.17.21", - "react": "^16", - "zen-logger": "^1.1.0" + "mobx": "^6.3.0", + "prop-types": "^15.8.1", + "react": "^16" }, "devDependencies": { "@alib/build-scripts": "^0.1.18", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^11.2.7", "@types/node": "^13.7.1", "@types/react": "^16", - "build-plugin-component": "^0.2.10" + "react-dom": "^16.14.0" }, "publishConfig": { "access": "public", @@ -33,5 +36,7 @@ "type": "http", "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/utils" }, - "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6" + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" } diff --git a/packages/utils/src/app-helper.ts b/packages/utils/src/app-helper.ts index d5eb2072b3..86b535c592 100644 --- a/packages/utils/src/app-helper.ts +++ b/packages/utils/src/app-helper.ts @@ -34,18 +34,18 @@ export class AppHelper extends EventEmitter { } } - batchOn(events: Array<string | symbol>, lisenter: (...args: any[]) => void) { + batchOn(events: Array<string | symbol>, listener: (...args: any[]) => void) { if (!Array.isArray(events)) return; - events.forEach((event) => this.on(event, lisenter)); + events.forEach((event) => this.on(event, listener)); } - batchOnce(events: Array<string | symbol>, lisenter: (...args: any[]) => void) { + batchOnce(events: Array<string | symbol>, listener: (...args: any[]) => void) { if (!Array.isArray(events)) return; - events.forEach((event) => this.once(event, lisenter)); + events.forEach((event) => this.once(event, listener)); } - batchOff(events: Array<string | symbol>, lisenter: (...args: any[]) => void) { + batchOff(events: Array<string | symbol>, listener: (...args: any[]) => void) { if (!Array.isArray(events)) return; - events.forEach((event) => this.off(event, lisenter)); + events.forEach((event) => this.off(event, listener)); } } diff --git a/packages/utils/src/asset.ts b/packages/utils/src/asset.ts index 1548e24004..3400f965b4 100644 --- a/packages/utils/src/asset.ts +++ b/packages/utils/src/asset.ts @@ -1,12 +1,12 @@ import { AssetType, AssetLevels, AssetLevel } from '@alilc/lowcode-types'; -import type { AssetItem, Asset, AssetList, AssetBundle, AssetsJson } from '@alilc/lowcode-types'; +import type { AssetItem, Asset, AssetList, AssetBundle, IPublicTypeAssetsJson } from '@alilc/lowcode-types'; import { isCSSUrl } from './is-css-url'; import { createDefer } from './create-defer'; import { load, evaluate } from './script'; // API 向下兼容 export { AssetType, AssetLevels, AssetLevel } from '@alilc/lowcode-types'; -export type { AssetItem, Asset, AssetList, AssetBundle, AssetsJson } from '@alilc/lowcode-types'; +export type { AssetItem, Asset, AssetList, AssetBundle, IPublicTypeAssetsJson } from '@alilc/lowcode-types'; export function isAssetItem(obj: any): obj is AssetItem { return obj && obj.type; @@ -16,7 +16,10 @@ export function isAssetBundle(obj: any): obj is AssetBundle { return obj && obj.type === AssetType.Bundle; } -export function assetBundle(assets?: Asset | AssetList | null, level?: AssetLevel): AssetBundle | null { +export function assetBundle( + assets?: Asset | AssetList | null, + level?: AssetLevel, + ): AssetBundle | null { if (!assets) { return null; } @@ -47,23 +50,22 @@ export function assetItem(type: AssetType, content?: string | null, level?: Asse }; } -export function megreAssets(assets: AssetsJson, incrementalAssets: AssetsJson): AssetsJson { +export function mergeAssets(assets: IPublicTypeAssetsJson, incrementalAssets: IPublicTypeAssetsJson): IPublicTypeAssetsJson { if (incrementalAssets.packages) { assets.packages = [...(assets.packages || []), ...incrementalAssets.packages]; } if (incrementalAssets.components) { - assets.components = [...assets.components, ...incrementalAssets.components]; + assets.components = [...(assets.components || []), ...incrementalAssets.components]; } - - megreAssetsComponentList(assets, incrementalAssets, 'componentList'); - megreAssetsComponentList(assets, incrementalAssets, 'bizComponentList'); + mergeAssetsComponentList(assets, incrementalAssets, 'componentList'); + mergeAssetsComponentList(assets, incrementalAssets, 'bizComponentList'); return assets; } -function megreAssetsComponentList(assets: AssetsJson, incrementalAssets: AssetsJson, listName: keyof AssetsJson): void { +function mergeAssetsComponentList(assets: IPublicTypeAssetsJson, incrementalAssets: IPublicTypeAssetsJson, listName: keyof IPublicTypeAssetsJson): void { if (incrementalAssets[listName]) { if (assets[listName]) { // 根据title进行合并 @@ -212,6 +214,8 @@ function parseAsset(scripts: any, styles: any, asset: Asset | undefined | null, } export class AssetLoader { + private stylePoints = new Map<string, StylePoint>(); + async load(asset: Asset) { const styles: any = {}; const scripts: any = {}; @@ -235,11 +239,9 @@ export class AssetLoader { await Promise.all( styleQueue.map(({ content, level, type, id }) => this.loadStyle(content, level!, type === AssetType.CSSUrl, id)), ); - await Promise.all(scriptQueue.map(({ content, type }) => this.loadScript(content, type === AssetType.JSUrl))); + await Promise.all(scriptQueue.map(({ content, type, scriptType }) => this.loadScript(content, type === AssetType.JSUrl, scriptType))); } - private stylePoints = new Map<string, StylePoint>(); - private loadStyle(content: string | undefined | null, level: AssetLevel, isUrl?: boolean, id?: string) { if (!content) { return; @@ -257,28 +259,35 @@ export class AssetLoader { return isUrl ? point.applyUrl(content) : point.applyText(content); } - private loadScript(content: string | undefined | null, isUrl?: boolean) { + private loadScript(content: string | undefined | null, isUrl?: boolean, scriptType?: string) { if (!content) { return; } - return isUrl ? load(content) : evaluate(content); + return isUrl ? load(content, scriptType) : evaluate(content, scriptType); } // todo 补充类型 async loadAsyncLibrary(asyncLibraryMap: Record<string, any>) { const promiseList: any[] = []; const libraryKeyList: any[] = []; + const pkgs: any[] = []; for (const key in asyncLibraryMap) { // 需要异步加载 if (asyncLibraryMap[key].async) { promiseList.push(window[asyncLibraryMap[key].library]); libraryKeyList.push(asyncLibraryMap[key].library); + pkgs.push(asyncLibraryMap[key]); } } await Promise.all(promiseList).then((mods) => { if (mods.length > 0) { mods.map((item, index) => { - window[libraryKeyList[index]] = item; + const { exportMode, exportSourceLibrary, library } = pkgs[index]; + window[libraryKeyList[index]] = + exportMode === 'functionCall' && + (exportSourceLibrary == null || exportSourceLibrary === library) + ? item() + : item; return item; }); } diff --git a/packages/utils/src/build-components.ts b/packages/utils/src/build-components.ts index c18952af12..909248524f 100644 --- a/packages/utils/src/build-components.ts +++ b/packages/utils/src/build-components.ts @@ -1,10 +1,12 @@ import { ComponentType, forwardRef, createElement, FunctionComponent } from 'react'; -import { NpmInfo, ComponentSchema } from '@alilc/lowcode-types'; -import { Component } from '@alilc/lowcode-designer'; +import { IPublicTypeNpmInfo, IPublicTypeComponentSchema, IPublicTypeProjectSchema } from '@alilc/lowcode-types'; import { isESModule } from './is-es-module'; import { isReactComponent, acceptsRef, wrapReactClass } from './is-react'; import { isObject } from './is-object'; +import { isLowcodeProjectSchema } from './check-types'; +import { isComponentSchema } from './check-types/is-component-schema'; +type Component = ComponentType<any> | object; interface LibraryMap { [key: string]: string; } @@ -36,7 +38,7 @@ export function getSubComponent(library: any, paths: string[]) { const key = paths[i]!; let ex: any; try { - component = library[key]; + component = library[key] || component; } catch (e) { ex = e; component = null; @@ -55,7 +57,7 @@ export function getSubComponent(library: any, paths: string[]) { return component; } -function findComponent(libraryMap: LibraryMap, componentName: string, npm?: NpmInfo) { +function findComponent(libraryMap: LibraryMap, componentName: string, npm?: IPublicTypeNpmInfo) { if (!npm) { return accessLibrary(componentName); } @@ -94,13 +96,21 @@ function isMixinComponent(components: any) { } export function buildComponents(libraryMap: LibraryMap, - componentsMap: { [componentName: string]: NpmInfo | ComponentType<any> | ComponentSchema }, - createComponent: (schema: ComponentSchema) => Component | null) { + componentsMap: { [componentName: string]: IPublicTypeNpmInfo | ComponentType<any> | IPublicTypeComponentSchema }, + createComponent: (schema: IPublicTypeProjectSchema<IPublicTypeComponentSchema>) => Component | null) { const components: any = {}; Object.keys(componentsMap).forEach((componentName) => { let component = componentsMap[componentName]; - if (component && (component as ComponentSchema).componentName === 'Component') { - components[componentName] = createComponent(component as ComponentSchema); + if (component && (isLowcodeProjectSchema(component) || isComponentSchema(component))) { + if (isComponentSchema(component)) { + components[componentName] = createComponent({ + version: '', + componentsMap: [], + componentsTree: [component], + }); + } else { + components[componentName] = createComponent(component); + } } else if (isReactComponent(component)) { if (!acceptsRef(component)) { component = wrapReactClass(component as FunctionComponent); @@ -111,7 +121,7 @@ export function buildComponents(libraryMap: LibraryMap, } else { component = findComponent(libraryMap, componentName, component); if (component) { - if (!acceptsRef(component)) { + if (!acceptsRef(component) && isReactComponent(component)) { component = wrapReactClass(component as FunctionComponent); } components[componentName] = component; 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 new file mode 100644 index 0000000000..507259b2c5 --- /dev/null +++ b/packages/utils/src/check-types/index.ts @@ -0,0 +1,28 @@ +// 此模块存放 @alilc/lowcode-types 中类型相关判断工具 +export * from './is-action-content-object'; +export * from './is-custom-view'; +export * from './is-dom-text'; +export * from './is-dynamic-setter'; +export * from './is-i18n-data'; +export * from './is-jsblock'; +export * from './is-jsexpression'; +export * from './is-isfunction'; +export * from './is-jsslot'; +export * from './is-lowcode-component-type'; +export * from './is-node-schema'; +export * from './is-procode-component-type'; +export * from './is-project-schema'; +export * from './is-setter-config'; +export * from './is-title-config'; +export * from './is-drag-node-data-object'; +export * from './is-drag-node-object'; +export * from './is-drag-any-object'; +export * from './is-location-children-detail'; +export * from './is-node'; +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'; +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-action-content-object.ts b/packages/utils/src/check-types/is-action-content-object.ts new file mode 100644 index 0000000000..8fe31b5bd7 --- /dev/null +++ b/packages/utils/src/check-types/is-action-content-object.ts @@ -0,0 +1,6 @@ +import { IPublicTypeActionContentObject } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isActionContentObject(obj: any): obj is IPublicTypeActionContentObject { + return isObject(obj); +} 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-component-schema.ts b/packages/utils/src/check-types/is-component-schema.ts new file mode 100644 index 0000000000..508d153b93 --- /dev/null +++ b/packages/utils/src/check-types/is-component-schema.ts @@ -0,0 +1,8 @@ +import { IPublicTypeComponentSchema } from "@alilc/lowcode-types"; + +export function isComponentSchema(schema: any): schema is IPublicTypeComponentSchema { + if (typeof schema === 'object') { + return schema.componentName === 'Component'; + } + return false +} diff --git a/packages/utils/src/check-types/is-custom-view.ts b/packages/utils/src/check-types/is-custom-view.ts new file mode 100644 index 0000000000..4cf921d9c5 --- /dev/null +++ b/packages/utils/src/check-types/is-custom-view.ts @@ -0,0 +1,10 @@ +import { isValidElement } from 'react'; +import { isReactComponent } from '../is-react'; +import { IPublicTypeCustomView } from '@alilc/lowcode-types'; + +export function isCustomView(obj: any): obj is IPublicTypeCustomView { + if (!obj) { + return false; + } + return isValidElement(obj) || isReactComponent(obj); +} diff --git a/packages/utils/src/check-types/is-dom-text.ts b/packages/utils/src/check-types/is-dom-text.ts new file mode 100644 index 0000000000..9509544409 --- /dev/null +++ b/packages/utils/src/check-types/is-dom-text.ts @@ -0,0 +1,3 @@ +export function isDOMText(data: any): data is string { + return typeof data === 'string'; +} diff --git a/packages/utils/src/check-types/is-drag-any-object.ts b/packages/utils/src/check-types/is-drag-any-object.ts new file mode 100644 index 0000000000..8711b4e333 --- /dev/null +++ b/packages/utils/src/check-types/is-drag-any-object.ts @@ -0,0 +1,9 @@ +import { IPublicEnumDragObjectType } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isDragAnyObject(obj: any): boolean { + if (!isObject(obj)) { + return false; + } + return obj.type !== IPublicEnumDragObjectType.NodeData && obj.type !== IPublicEnumDragObjectType.Node; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-drag-node-data-object.ts b/packages/utils/src/check-types/is-drag-node-data-object.ts new file mode 100644 index 0000000000..aa62f5b1c9 --- /dev/null +++ b/packages/utils/src/check-types/is-drag-node-data-object.ts @@ -0,0 +1,9 @@ +import { IPublicEnumDragObjectType, IPublicTypeDragNodeDataObject } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isDragNodeDataObject(obj: any): obj is IPublicTypeDragNodeDataObject { + if (!isObject(obj)) { + return false; + } + return obj.type === IPublicEnumDragObjectType.NodeData; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-drag-node-object.ts b/packages/utils/src/check-types/is-drag-node-object.ts new file mode 100644 index 0000000000..3a29ec967f --- /dev/null +++ b/packages/utils/src/check-types/is-drag-node-object.ts @@ -0,0 +1,9 @@ +import { IPublicEnumDragObjectType, IPublicModelNode, IPublicTypeDragNodeObject } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isDragNodeObject<Node = IPublicModelNode>(obj: any): obj is IPublicTypeDragNodeObject<Node> { + if (!isObject(obj)) { + return false; + } + return obj.type === IPublicEnumDragObjectType.Node; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-dynamic-setter.ts b/packages/utils/src/check-types/is-dynamic-setter.ts new file mode 100644 index 0000000000..35f8ff3892 --- /dev/null +++ b/packages/utils/src/check-types/is-dynamic-setter.ts @@ -0,0 +1,10 @@ +import { isFunction } from '../is-function'; +import { isReactClass } from '../is-react'; +import { IPublicTypeDynamicSetter } from '@alilc/lowcode-types'; + +export function isDynamicSetter(obj: any): obj is IPublicTypeDynamicSetter { + if (!isFunction(obj)) { + return false; + } + return !isReactClass(obj); +} diff --git a/packages/utils/src/check-types/is-function.ts b/packages/utils/src/check-types/is-function.ts new file mode 100644 index 0000000000..d7d3b4c27d --- /dev/null +++ b/packages/utils/src/check-types/is-function.ts @@ -0,0 +1,3 @@ +export function isFunction(obj: any): obj is Function { + return obj && typeof obj === 'function'; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-i18n-data.ts b/packages/utils/src/check-types/is-i18n-data.ts new file mode 100644 index 0000000000..793295d240 --- /dev/null +++ b/packages/utils/src/check-types/is-i18n-data.ts @@ -0,0 +1,9 @@ +import { IPublicTypeI18nData } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isI18nData(obj: any): obj is IPublicTypeI18nData { + if (!isObject(obj)) { + return false; + } + return obj.type === 'i18n'; +} diff --git a/packages/utils/src/check-types/is-isfunction.ts b/packages/utils/src/check-types/is-isfunction.ts new file mode 100644 index 0000000000..64b8676637 --- /dev/null +++ b/packages/utils/src/check-types/is-isfunction.ts @@ -0,0 +1,26 @@ +import { IPublicTypeJSFunction } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +interface InnerJsFunction { + type: 'JSExpression'; + source: string; + value: string; + extType: 'function'; +} + +/** + * 内部版本 的 { type: 'JSExpression', source: '', value: '', extType: 'function' } 能力上等同于 JSFunction + */ +export function isInnerJsFunction(data: any): data is InnerJsFunction { + if (!isObject(data)) { + return false; + } + return data.type === 'JSExpression' && data.extType === 'function'; +} + +export function isJSFunction(data: any): data is IPublicTypeJSFunction { + if (!isObject(data)) { + return false; + } + return data.type === 'JSFunction' || isInnerJsFunction(data); +} diff --git a/packages/utils/src/check-types/is-jsblock.ts b/packages/utils/src/check-types/is-jsblock.ts new file mode 100644 index 0000000000..858f5c09cd --- /dev/null +++ b/packages/utils/src/check-types/is-jsblock.ts @@ -0,0 +1,9 @@ +import { IPublicTypeJSBlock } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isJSBlock(data: any): data is IPublicTypeJSBlock { + if (!isObject(data)) { + return false; + } + return data.type === 'JSBlock'; +} diff --git a/packages/utils/src/check-types/is-jsexpression.ts b/packages/utils/src/check-types/is-jsexpression.ts new file mode 100644 index 0000000000..16b8f4ac2a --- /dev/null +++ b/packages/utils/src/check-types/is-jsexpression.ts @@ -0,0 +1,19 @@ +import { IPublicTypeJSExpression } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +/** + * 为了避免把 { type: 'JSExpression', extType: 'function' } 误判为表达式,故增加如下逻辑。 + * + * 引擎中关于函数的表达: + * 开源版本:{ type: 'JSFunction', source: '', value: '' } + * 内部版本:{ type: 'JSExpression', source: '', value: '', extType: 'function' } + * 能力是对标的,不过开源的 react-renderer 只认识第一种,而内部只识别第二种(包括 Java 代码、RE)。 + * @param data + * @returns + */ +export function isJSExpression(data: any): data is IPublicTypeJSExpression { + if (!isObject(data)) { + return false; + } + return data.type === 'JSExpression' && data.extType !== 'function'; +} diff --git a/packages/utils/src/check-types/is-jsslot.ts b/packages/utils/src/check-types/is-jsslot.ts new file mode 100644 index 0000000000..1fb1d819d7 --- /dev/null +++ b/packages/utils/src/check-types/is-jsslot.ts @@ -0,0 +1,9 @@ +import { IPublicTypeJSSlot } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isJSSlot(data: any): data is IPublicTypeJSSlot { + if (!isObject(data)) { + return false; + } + return data.type === 'JSSlot'; +} diff --git a/packages/utils/src/check-types/is-location-children-detail.ts b/packages/utils/src/check-types/is-location-children-detail.ts new file mode 100644 index 0000000000..cc093c4e4a --- /dev/null +++ b/packages/utils/src/check-types/is-location-children-detail.ts @@ -0,0 +1,9 @@ +import { IPublicTypeLocationChildrenDetail, IPublicTypeLocationDetailType } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isLocationChildrenDetail(obj: any): obj is IPublicTypeLocationChildrenDetail { + if (!isObject(obj)) { + return false; + } + return obj.type === IPublicTypeLocationDetailType.Children; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-location-data.ts b/packages/utils/src/check-types/is-location-data.ts new file mode 100644 index 0000000000..dabd493fa8 --- /dev/null +++ b/packages/utils/src/check-types/is-location-data.ts @@ -0,0 +1,9 @@ +import { IPublicTypeLocationData } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isLocationData(obj: any): obj is IPublicTypeLocationData { + if (!isObject(obj)) { + return false; + } + return 'target' in obj && 'detail' in obj; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-lowcode-component-type.ts b/packages/utils/src/check-types/is-lowcode-component-type.ts new file mode 100644 index 0000000000..ce19c23e87 --- /dev/null +++ b/packages/utils/src/check-types/is-lowcode-component-type.ts @@ -0,0 +1,7 @@ +import { isProCodeComponentType } from './is-procode-component-type'; +import { IPublicTypeComponentMap, IPublicTypeLowCodeComponent } from '@alilc/lowcode-types'; + + +export function isLowCodeComponentType(desc: IPublicTypeComponentMap): desc is IPublicTypeLowCodeComponent { + return !isProCodeComponentType(desc); +} diff --git a/packages/utils/src/check-types/is-lowcode-project-schema.ts b/packages/utils/src/check-types/is-lowcode-project-schema.ts new file mode 100644 index 0000000000..230911f0f3 --- /dev/null +++ b/packages/utils/src/check-types/is-lowcode-project-schema.ts @@ -0,0 +1,15 @@ +import { IPublicTypeComponentSchema, IPublicTypeProjectSchema } from '@alilc/lowcode-types'; +import { isComponentSchema } from './is-component-schema'; +import { isObject } from '../is-object'; + +export function isLowcodeProjectSchema(data: any): data is IPublicTypeProjectSchema<IPublicTypeComponentSchema> { + if (!isObject(data)) { + return false; + } + + if (!('componentsTree' in data) || data.componentsTree.length === 0) { + return false; + } + + return isComponentSchema(data.componentsTree[0]); +} diff --git a/packages/utils/src/check-types/is-node-schema.ts b/packages/utils/src/check-types/is-node-schema.ts new file mode 100644 index 0000000000..253c05a080 --- /dev/null +++ b/packages/utils/src/check-types/is-node-schema.ts @@ -0,0 +1,9 @@ +import { IPublicTypeNodeSchema } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isNodeSchema(data: any): data is IPublicTypeNodeSchema { + if (!isObject(data)) { + return false; + } + return 'componentName' in data && !data.isNode; +} diff --git a/packages/utils/src/check-types/is-node.ts b/packages/utils/src/check-types/is-node.ts new file mode 100644 index 0000000000..b4690ddff9 --- /dev/null +++ b/packages/utils/src/check-types/is-node.ts @@ -0,0 +1,9 @@ +import { IPublicModelNode } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isNode<Node = IPublicModelNode>(node: any): node is Node { + if (!isObject(node)) { + return false; + } + return node.isNode; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-object.ts b/packages/utils/src/check-types/is-object.ts new file mode 100644 index 0000000000..56ceb7d979 --- /dev/null +++ b/packages/utils/src/check-types/is-object.ts @@ -0,0 +1,3 @@ +export function isObject(obj: any): boolean { + return obj && typeof obj === 'object'; +} \ No newline at end of file diff --git a/packages/utils/src/check-types/is-procode-component-type.ts b/packages/utils/src/check-types/is-procode-component-type.ts new file mode 100644 index 0000000000..46618dcd5a --- /dev/null +++ b/packages/utils/src/check-types/is-procode-component-type.ts @@ -0,0 +1,10 @@ +import { IPublicTypeComponentMap, IPublicTypeProCodeComponent } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isProCodeComponentType(desc: IPublicTypeComponentMap): desc is IPublicTypeProCodeComponent { + if (!isObject(desc)) { + return false; + } + + return 'package' in desc; +} diff --git a/packages/utils/src/check-types/is-project-schema.ts b/packages/utils/src/check-types/is-project-schema.ts new file mode 100644 index 0000000000..d217acd9ee --- /dev/null +++ b/packages/utils/src/check-types/is-project-schema.ts @@ -0,0 +1,9 @@ +import { IPublicTypeProjectSchema } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isProjectSchema(data: any): data is IPublicTypeProjectSchema { + if (!isObject(data)) { + return false; + } + return 'componentsTree' in data; +} 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/check-types/is-setter-config.ts b/packages/utils/src/check-types/is-setter-config.ts new file mode 100644 index 0000000000..98d835f32c --- /dev/null +++ b/packages/utils/src/check-types/is-setter-config.ts @@ -0,0 +1,10 @@ +import { IPublicTypeSetterConfig } from '@alilc/lowcode-types'; +import { isCustomView } from './is-custom-view'; +import { isObject } from '../is-object'; + +export function isSetterConfig(obj: any): obj is IPublicTypeSetterConfig { + if (!isObject(obj)) { + return false; + } + return 'componentName' in obj && !isCustomView(obj); +} diff --git a/packages/utils/src/check-types/is-setting-field.ts b/packages/utils/src/check-types/is-setting-field.ts new file mode 100644 index 0000000000..0d6e21d848 --- /dev/null +++ b/packages/utils/src/check-types/is-setting-field.ts @@ -0,0 +1,10 @@ +import { IPublicModelSettingField } from '@alilc/lowcode-types'; +import { isObject } from '../is-object'; + +export function isSettingField(obj: any): obj is IPublicModelSettingField { + if (!isObject(obj)) { + return false; + } + + return 'isSettingField' in obj && obj.isSettingField; +} diff --git a/packages/utils/src/check-types/is-title-config.ts b/packages/utils/src/check-types/is-title-config.ts new file mode 100644 index 0000000000..460da99790 --- /dev/null +++ b/packages/utils/src/check-types/is-title-config.ts @@ -0,0 +1,7 @@ +import { IPublicTypeTitleConfig } from '@alilc/lowcode-types'; +import { isI18nData } from './is-i18n-data'; +import { isPlainObject } from '../is-plain-object'; + +export function isTitleConfig(obj: any): obj is IPublicTypeTitleConfig { + return isPlainObject(obj) && !isI18nData(obj); +} diff --git a/packages/utils/src/clone-enumerable-property.ts b/packages/utils/src/clone-enumerable-property.ts index 414f8dccdd..eb09e177fc 100644 --- a/packages/utils/src/clone-enumerable-property.ts +++ b/packages/utils/src/clone-enumerable-property.ts @@ -11,8 +11,8 @@ const excludePropertyNames = [ 'arguments', ]; -export function cloneEnumerableProperty(target: any, origin: any) { - const compExtraPropertyNames = Object.keys(origin).filter(d => !excludePropertyNames.includes(d)); +export function cloneEnumerableProperty(target: any, origin: any, excludes = excludePropertyNames) { + const compExtraPropertyNames = Object.keys(origin).filter(d => !excludes.includes(d)); compExtraPropertyNames.forEach((d: string) => { (target as any)[d] = origin[d]; diff --git a/packages/utils/src/context-menu.scss b/packages/utils/src/context-menu.scss new file mode 100644 index 0000000000..0b75ca3ec1 --- /dev/null +++ b/packages/utils/src/context-menu.scss @@ -0,0 +1,50 @@ +.engine-context-menu-tree-wrap { + position: relative; + padding: 4px 10px 4px 32px; +} + +.engine-context-menu-tree-children { + margin-left: 8px; + line-height: 24px; +} + +.engine-context-menu-item { + .engine-context-menu-text { + color: var(--color-context-menu-text, var(--color-text)); + display: flex; + align-items: center; + + .lc-help-tip { + margin-left: 4px; + opacity: 0.8; + } + } + + &.disabled { + &:hover .engine-context-menu-text, .engine-context-menu-text { + color: var(--color-context-menu-text-disabled, var(--color-text-disabled)); + } + } + + &:hover { + .engine-context-menu-text { + color: var(--color-context-menu-text-hover, var(--color-title)); + } + } +} + +.engine-context-menu-title { + color: var(--color-context-menu-text, var(--color-text)); + cursor: pointer; + + &:hover { + background-color: var(--color-block-background-light); + color: var(--color-title); + } +} + +.engine-context-menu-tree-selecte-icon { + position: absolute; + left: 10px; + color: var(--color-icon-active); +} \ No newline at end of file diff --git a/packages/utils/src/context-menu.tsx b/packages/utils/src/context-menu.tsx new file mode 100644 index 0000000000..185abbb343 --- /dev/null +++ b/packages/utils/src/context-menu.tsx @@ -0,0 +1,230 @@ +import { Menu, Icon } from '@alifd/next'; +import { IPublicEnumContextMenuType, IPublicModelNode, IPublicModelPluginContext, IPublicTypeContextMenuAction, IPublicTypeContextMenuItem } from '@alilc/lowcode-types'; +import { Logger } from '@alilc/lowcode-utils'; +import classNames from 'classnames'; +import React from 'react'; +import './context-menu.scss'; + +const logger = new Logger({ level: 'warn', bizName: 'utils' }); +const { Item, Divider, PopupItem } = Menu; + +const MAX_LEVEL = 2; + +interface IOptions { + nodes?: IPublicModelNode[] | null; + destroy?: Function; + pluginContext: IPublicModelPluginContext; +} + +const Tree = (props: { + node?: IPublicModelNode | null; + children?: React.ReactNode; + options: IOptions; +}) => { + const { node } = props; + + if (!node) { + return ( + <div className="engine-context-menu-tree-wrap">{ props.children }</div> + ); + } + + const { common } = props.options.pluginContext || {}; + const { intl } = common?.utils || {}; + const indent = node.zLevel * 8 + 32; + const style = { + paddingLeft: indent, + marginLeft: -indent, + marginRight: -10, + paddingRight: 10, + }; + + return ( + <Tree {...props} node={node.parent} > + <div + className="engine-context-menu-title" + onClick={() => { + props.options.destroy?.(); + node.select(); + }} + style={style} + > + {props.options.nodes?.[0].id === node.id ? (<Icon className="engine-context-menu-tree-selecte-icon" size="small" type="success" />) : null} + {intl(node.title)} + </div> + <div + className="engine-context-menu-tree-children" + > + { props.children } + </div> + </Tree> + ); +}; + +let destroyFn: Function | undefined; + +export function parseContextMenuAsReactNode(menus: IPublicTypeContextMenuItem[], options: IOptions): React.ReactNode[] { + const { common, commonUI } = options.pluginContext || {}; + const { intl = (title: any) => title } = common?.utils || {}; + const { HelpTip } = commonUI || {}; + + const children: React.ReactNode[] = []; + menus.forEach((menu, index) => { + if (menu.type === IPublicEnumContextMenuType.SEPARATOR) { + children.push(<Divider key={menu.name || index} />); + return; + } + + if (menu.type === IPublicEnumContextMenuType.MENU_ITEM) { + if (menu.items && menu.items.length) { + children.push(( + <PopupItem + className={classNames('engine-context-menu-item', { + disabled: menu.disabled, + })} + key={menu.name} + label={<div className="engine-context-menu-text">{intl(menu.title)}</div>} + > + <Menu className="next-context engine-context-menu"> + { parseContextMenuAsReactNode(menu.items, options) } + </Menu> + </PopupItem> + )); + } else { + children.push(( + <Item + className={classNames('engine-context-menu-item', { + disabled: menu.disabled, + })} + disabled={menu.disabled} + onClick={() => { + menu.action?.(); + }} + key={menu.name} + > + <div className="engine-context-menu-text"> + { menu.title ? intl(menu.title) : null } + { menu.help ? <HelpTip size="xs" help={menu.help} direction="right" /> : null } + </div> + </Item> + )); + } + } + + if (menu.type === IPublicEnumContextMenuType.NODE_TREE) { + children.push(( + <Tree node={options.nodes?.[0]} options={options} /> + )); + } + }); + + return children; +} + +export function parseContextMenuProperties(menus: (IPublicTypeContextMenuAction | Omit<IPublicTypeContextMenuAction, 'items'>)[], options: IOptions & { + event?: MouseEvent; +}, level = 1): IPublicTypeContextMenuItem[] { + destroyFn?.(); + + const { nodes, destroy } = options; + if (level > MAX_LEVEL) { + logger.warn('context menu level is too deep, please check your context menu config'); + return []; + } + + return menus + .filter(menu => !menu.condition || (menu.condition && menu.condition(nodes || []))) + .map((menu) => { + const { + name, + title, + type = IPublicEnumContextMenuType.MENU_ITEM, + help, + } = menu; + + const result: IPublicTypeContextMenuItem = { + name, + title, + type, + help, + action: () => { + destroy?.(); + menu.action?.(nodes || [], options.event); + }, + disabled: menu.disabled && menu.disabled(nodes || []) || false, + }; + + if ('items' in menu && menu.items) { + result.items = parseContextMenuProperties( + typeof menu.items === 'function' ? menu.items(nodes || []) : menu.items, + options, + level + 1, + ); + } + + return result; + }) + .reduce((menus: IPublicTypeContextMenuItem[], currentMenu: IPublicTypeContextMenuItem) => { + if (!currentMenu.name) { + return menus.concat([currentMenu]); + } + + const index = menus.find(item => item.name === currentMenu.name); + if (!index) { + return menus.concat([currentMenu]); + } else { + return menus; + } + }, []); +} + +let cachedMenuItemHeight: string | undefined; + +function getMenuItemHeight() { + if (cachedMenuItemHeight) { + return cachedMenuItemHeight; + } + const root = document.documentElement; + const styles = getComputedStyle(root); + const menuItemHeight = styles.getPropertyValue('--context-menu-item-height').trim(); + cachedMenuItemHeight = menuItemHeight; + + return menuItemHeight; +} + +export function createContextMenu(children: React.ReactNode[], { + event, + offset = [0, 0], +}: { + event: MouseEvent | React.MouseEvent; + offset?: [number, number]; +}) { + event.preventDefault(); + event.stopPropagation(); + + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + const dividerCount = React.Children.count(children.filter(child => React.isValidElement(child) && child.type === Divider)); + const popupItemCount = React.Children.count(children.filter(child => React.isValidElement(child) && (child.type === PopupItem || child.type === Item))); + const menuHeight = popupItemCount * parseInt(getMenuItemHeight(), 10) + dividerCount * 8 + 16; + const menuWidthLimit = 200; + let x = event.clientX + offset[0]; + let y = event.clientY + offset[1]; + if (x + menuWidthLimit > viewportWidth) { + x = x - menuWidthLimit; + } + if (y + menuHeight > viewportHeight) { + y = y - menuHeight; + } + + const menuInstance = Menu.create({ + target: document.body, + offset: [x, y], + children, + className: 'engine-context-menu', + }); + + destroyFn = (menuInstance as any).destroy; + + return destroyFn; +} \ No newline at end of file diff --git a/packages/utils/src/create-content.ts b/packages/utils/src/create-content.ts index 211c26f165..09a368d2b9 100644 --- a/packages/utils/src/create-content.ts +++ b/packages/utils/src/create-content.ts @@ -1,7 +1,10 @@ import { ReactNode, ComponentType, isValidElement, cloneElement, createElement } from 'react'; import { isReactComponent } from './is-react'; -export function createContent(content: ReactNode | ComponentType<any>, props?: Record<string, unknown>): ReactNode { +export function createContent( + content: ReactNode | ComponentType<any>, + props?: Record<string, unknown>, + ): ReactNode { if (isValidElement(content)) { return props ? cloneElement(content, props) : content; } diff --git a/packages/utils/src/create-icon.tsx b/packages/utils/src/create-icon.tsx index 0f1e72f4f3..621b5c7ab4 100644 --- a/packages/utils/src/create-icon.tsx +++ b/packages/utils/src/create-icon.tsx @@ -1,12 +1,15 @@ import { isValidElement, ReactNode, createElement, cloneElement } from 'react'; import { Icon } from '@alifd/next'; -import { IconType } from '@alilc/lowcode-types'; +import { IPublicTypeIconType } from '@alilc/lowcode-types'; import { isReactComponent } from './is-react'; import { isESModule } from './is-es-module'; const URL_RE = /^(https?:)\/\//i; -export function createIcon(icon?: IconType | null, props?: Record<string, unknown>): ReactNode { +export function createIcon( + icon?: IPublicTypeIconType | null, + props?: Record<string, unknown>, + ): ReactNode { if (!icon) { return null; } @@ -15,7 +18,11 @@ export function createIcon(icon?: IconType | null, props?: Record<string, unknow } if (typeof icon === 'string') { if (URL_RE.test(icon)) { - return <img src={icon} {...props} />; + return createElement('img', { + src: icon, + class: props?.className, + ...props, + }); } return <Icon type={icon} {...props} />; } @@ -23,7 +30,10 @@ export function createIcon(icon?: IconType | null, props?: Record<string, unknow return cloneElement(icon, { ...props }); } if (isReactComponent(icon)) { - return createElement(icon, { ...props }); + return createElement(icon, { + class: props?.className, + ...props, + }); } return <Icon {...icon} {...props} />; diff --git a/packages/utils/src/css-helper.ts b/packages/utils/src/css-helper.ts index 9858d0d54e..98bf1bbf0c 100644 --- a/packages/utils/src/css-helper.ts +++ b/packages/utils/src/css-helper.ts @@ -10,14 +10,13 @@ const pseudoMap = ['hover', 'focus', 'active', 'visited']; const RE_CAMEL = /[A-Z]/g; const RE_HYPHEN = /[-\s]+(.)?/g; -const CSS_REG = /:root(.*)\{.*/i; const PROPS_REG = /([^:]*):\s?(.*)/i; // 给 css 分组 -function groupingCss(css) { +function groupingCss(css: string) { let stackLength = 0; let startIndex = 0; - const group = []; + const group: string[] = []; css.split('').forEach((char, index) => { if (char === '{') { stackLength++; @@ -33,38 +32,38 @@ function groupingCss(css) { return group; } - -function isString(str) { +function isString(str: any): str is string { return {}.toString.call(str) === '[object String]'; } -function hyphenate(str) { +function hyphenate(str: string): string { return str.replace(RE_CAMEL, w => `-${w}`).toLowerCase(); } -function camelize(str) { +function camelize(str: string): string { return str.replace(RE_HYPHEN, (m, w) => (w ? w.toUpperCase() : '')); } + /** * convert * {background-color: "red"} * to * background-color: red; */ -function runtimeToCss(runtime) { - const css = []; +function runtimeToCss(runtime: Record<string, string>) { + const css: string[] = []; Object.keys(runtime).forEach((key) => { css.push(` ${key}: ${runtime[key]};`); }); return css.join('\n'); } -function toNativeStyle(runtime) { +function toNativeStyle(runtime: Record<string, string> | undefined) { if (!runtime) { return {}; } if (runtime.default) { - const normalized = {}; + const normalized: Record<string, string> = {}; Object.keys(runtime).forEach((pseudo) => { if (pseudo === 'extra') { normalized[pseudo] = runtime[pseudo]; @@ -98,14 +97,13 @@ function normalizeStyle(style) { return normalized; } - const normalized = {}; + const normalized: Record<string, string | Record<string, string>> = {}; Object.keys(style).forEach((key) => { normalized[hyphenate(key)] = style[key]; }); return normalized; } - function toCss(runtime) { if (!runtime) { return ( @@ -115,7 +113,7 @@ function toCss(runtime) { } if (runtime.default) { - const css = []; + const css: string[] = []; Object.keys(runtime).forEach((pseudo) => { if (pseudo === 'extra') { Array.isArray(runtime.extra) && css.push(runtime.extra.join('\n')); @@ -140,11 +138,14 @@ ${runtimeToCss(normalizeStyle(runtime))} ); } -function cssToRuntime(css) { +function cssToRuntime(css: string) { if (!css) { return {}; } - const runtime = {}; + const runtime: { + extra?: string[]; + default?: Record<string, string>; + } = {}; const groups = groupingCss(css); groups.forEach((cssItem) => { if (!cssItem.startsWith(':root')) { @@ -153,7 +154,7 @@ function cssToRuntime(css) { } else { const res = /:root:?(.*)?{(.*)/ig.exec(cssItem.replace(/[\r\n]+/ig, '').trim()); if (res) { - let pseudo; + let pseudo: string | undefined; if (res[1] && res[1].trim() && some(pseudoMap, pse => res[1].indexOf(pse) === 0)) { pseudo = res[1].trim(); @@ -161,8 +162,8 @@ function cssToRuntime(css) { pseudo = res[1]; } - const s = {}; - res[2].split(';').reduce((prev, next) => { + const s: Record<string, string> = {}; + res[2].split(';').reduce<string[]>((prev, next) => { if (next.indexOf('base64') > -1) { prev[prev.length - 1] += `;${next}`; } else { @@ -173,8 +174,8 @@ function cssToRuntime(css) { if (item) { if (PROPS_REG.test(item)) { const props = item.match(PROPS_REG); - const key = props[1]; - const value = props[2]; + const key = props?.[1]; + const value = props?.[2]; if (key && value) { s[key.trim()] = value.trim(); } @@ -182,10 +183,7 @@ function cssToRuntime(css) { } }); - if (!pseudo) { - pseudo = 'default'; - } - runtime[pseudo] = s; + runtime[pseudo || 'default'] = s; } } }); diff --git a/packages/utils/src/cursor.css b/packages/utils/src/cursor.css new file mode 100644 index 0000000000..e13da656ea --- /dev/null +++ b/packages/utils/src/cursor.css @@ -0,0 +1,19 @@ +html.lc-cursor-dragging, +html.lc-cursor-dragging * { + cursor: move !important; +} + +html.lc-cursor-x-resizing, +html.lc-cursor-x-resizing * { + cursor: col-resize; +} + +html.lc-cursor-y-resizing, +html.lc-cursor-y-resizing * { + cursor: row-resize; +} + +html.lc-cursor-copy, +html.lc-cursor-copy * { + cursor: copy !important; +} diff --git a/packages/utils/src/cursor.less b/packages/utils/src/cursor.less deleted file mode 100644 index 30c890862e..0000000000 --- a/packages/utils/src/cursor.less +++ /dev/null @@ -1,15 +0,0 @@ -html.lc-cursor-dragging, html.lc-cursor-dragging * { - cursor: move !important; -} - -html.lc-cursor-x-resizing, html.lc-cursor-x-resizing * { - cursor: col-resize; -} - -html.lc-cursor-y-resizing, html.lc-cursor-y-resizing * { - cursor: row-resize; -} - -html.lc-cursor-copy, html.lc-cursor-copy * { - cursor: copy !important; -} diff --git a/packages/utils/src/cursor.ts b/packages/utils/src/cursor.ts index fea4bce65b..c12ec64b92 100644 --- a/packages/utils/src/cursor.ts +++ b/packages/utils/src/cursor.ts @@ -1,4 +1,4 @@ -import './cursor.less'; +import './cursor.css'; export class Cursor { private states = new Set<string>(); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 1277f44818..22bad0e36e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -25,4 +25,11 @@ export * from './schema'; export * from './node-helper'; export * from './clone-enumerable-property'; export * from './logger'; +export * from './is-shaken'; +export * from './is-plugin-event-name'; export * as css from './css-helper'; +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/src/is-object.ts b/packages/utils/src/is-object.ts index 50b580e5a1..c8d764458b 100644 --- a/packages/utils/src/is-object.ts +++ b/packages/utils/src/is-object.ts @@ -1,4 +1,4 @@ -export function isObject(value: any): value is Record<string, unknown> { +export function isObject(value: any): value is Record<string, any> { return value !== null && typeof value === 'object'; } diff --git a/packages/utils/src/is-plugin-event-name.ts b/packages/utils/src/is-plugin-event-name.ts new file mode 100644 index 0000000000..688eddc6e8 --- /dev/null +++ b/packages/utils/src/is-plugin-event-name.ts @@ -0,0 +1,8 @@ +export function isPluginEventName(eventName: string): boolean { + if (!eventName) { + return false; + } + + const eventSegments = eventName.split(':'); + return (eventSegments.length > 1 && eventSegments[0].length > 0); +} diff --git a/packages/utils/src/is-react.ts b/packages/utils/src/is-react.ts index 02ef50fa67..1d6c939ea1 100644 --- a/packages/utils/src/is-react.ts +++ b/packages/utils/src/is-react.ts @@ -2,28 +2,69 @@ import { ComponentClass, Component, FunctionComponent, ComponentType, createElem import { cloneEnumerableProperty } from './clone-enumerable-property'; const hasSymbol = typeof Symbol === 'function' && Symbol.for; -const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; +export const REACT_FORWARD_REF_TYPE = hasSymbol ? Symbol.for('react.forward_ref') : 0xead0; +export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; export function isReactClass(obj: any): obj is ComponentClass<any> { - return obj && obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component); + if (!obj) { + return false; + } + if (obj.prototype && (obj.prototype.isReactComponent || obj.prototype instanceof Component)) { + return true; + } + return false; } export function acceptsRef(obj: any): boolean { - return obj?.prototype?.isReactComponent || (obj.$$typeof && obj.$$typeof === REACT_FORWARD_REF_TYPE); + if (!obj) { + return false; + } + if (obj?.prototype?.isReactComponent || isForwardOrMemoForward(obj)) { + return true; + } + + return false; +} + +export function isForwardRefType(obj: any): boolean { + if (!obj || !obj?.$$typeof) { + return false; + } + return obj?.$$typeof === REACT_FORWARD_REF_TYPE; } -function isForwardRefType(obj: any): boolean { - return obj?.$$typeof && obj?.$$typeof === REACT_FORWARD_REF_TYPE; +export function isMemoType(obj: any): boolean { + if (!obj || !obj?.$$typeof) { + return false; + } + return obj.$$typeof === REACT_MEMO_TYPE; +} + +export function isForwardOrMemoForward(obj: any): boolean { + if (!obj || !obj?.$$typeof) { + return false; + } + return ( + // React.forwardRef(..) + isForwardRefType(obj) || + // React.memo(React.forwardRef(..)) + (isMemoType(obj) && isForwardRefType(obj.type)) + ); } export function isReactComponent(obj: any): obj is ComponentType<any> { - return obj && (isReactClass(obj) || typeof obj === 'function' || isForwardRefType(obj)); + if (!obj) { + return false; + } + + return Boolean(isReactClass(obj) || typeof obj === 'function' || isForwardRefType(obj) || isMemoType(obj)); } export function wrapReactClass(view: FunctionComponent) { let ViewComponentClass = class extends Component { render() { - return createElement(view, this.props); + const { children, ...other } = this.props; + return createElement(view, other, children); } } as any; ViewComponentClass = cloneEnumerableProperty(ViewComponentClass, view); diff --git a/packages/utils/src/is-shaken.ts b/packages/utils/src/is-shaken.ts new file mode 100644 index 0000000000..6c4606e712 --- /dev/null +++ b/packages/utils/src/is-shaken.ts @@ -0,0 +1,15 @@ +const SHAKE_DISTANCE = 4; +/** + * mouse shake check + */ +export function isShaken(e1: MouseEvent | DragEvent, e2: MouseEvent | DragEvent): boolean { + if ((e1 as any).shaken) { + return true; + } + if (e1.target !== e2.target) { + return true; + } + return ( + Math.pow(e1.clientY - e2.clientY, 2) + Math.pow(e1.clientX - e2.clientX, 2) > SHAKE_DISTANCE + ); +} \ No newline at end of file diff --git a/packages/utils/src/logger.ts b/packages/utils/src/logger.ts index 47ec22c6f1..3eb43eedbe 100644 --- a/packages/utils/src/logger.ts +++ b/packages/utils/src/logger.ts @@ -1,4 +1,195 @@ -import Logger, { Level } from 'zen-logger'; +/* eslint-disable no-console */ +/* eslint-disable no-param-reassign */ +import { isObject } from './is-object'; + +export type Level = 'debug' | 'log' | 'info' | 'warn' | 'error'; +interface Options { + level: Level; + bizName: string; +} + +const levels: Record<string, number> = { + debug: -1, + log: 0, + info: 0, + warn: 1, + error: 2, +}; +const bizNameColors = [ + '#daa569', + '#00ffff', + '#385e0f', + '#7fffd4', + '#00c957', + '#b0e0e6', + '#4169e1', + '#6a5acd', + '#87ceeb', + '#ffff00', + '#e3cf57', + '#ff9912', + '#eb8e55', + '#ffe384', + '#40e0d0', + '#a39480', + '#d2691e', + '#ff7d40', + '#f0e68c', + '#bc8f8f', + '#c76114', + '#734a12', + '#5e2612', + '#0000ff', + '#3d59ab', + '#1e90ff', + '#03a89e', + '#33a1c9', + '#a020f0', + '#a066d3', + '#da70d6', + '#dda0dd', + '#688e23', + '#2e8b57', +]; +const bodyColors: Record<string, string> = { + debug: '#fadb14', + log: '#8c8c8c', + info: '#52c41a', + warn: '#fa8c16', + error: '#ff4d4f', +}; +const levelMarks: Record<string, string> = { + debug: 'debug', + log: 'log', + info: 'info', + warn: 'warn', + error: 'error', +}; +const outputFunction: Record<string, any> = { + debug: console.log, + log: console.log, + info: console.log, + warn: console.warn, + error: console.error, +}; + +const bizNameColorConfig: Record<string, string> = {}; + +const shouldOutput = ( + logLevel: string, + targetLevel: string = 'warn', + bizName: string, + targetBizName: string, + ): boolean => { + const isLevelFit = (levels as any)[targetLevel] <= (levels as any)[logLevel]; + const isBizNameFit = targetBizName === '*' || bizName.indexOf(targetBizName) > -1; + return isLevelFit && isBizNameFit; +}; + +const output = (logLevel: string, bizName: string) => { + return (...args: any[]) => { + return outputFunction[logLevel]?.apply(console, getLogArgs(args, bizName, logLevel)); + }; +}; + +const getColor = (bizName: string) => { + if (!bizNameColorConfig[bizName]) { + const color = bizNameColors[Object.keys(bizNameColorConfig).length % bizNameColors.length]; + bizNameColorConfig[bizName] = color; + } + return bizNameColorConfig[bizName]; +}; + +const getLogArgs = (args: any, bizName: string, logLevel: string) => { + const color = getColor(bizName); + const bodyColor = bodyColors[logLevel]; + + const argsArray = args[0]; + let prefix = `%c[${bizName}]%c[${levelMarks[logLevel]}]:`; + argsArray.forEach((arg: any) => { + if (isObject(arg)) { + prefix += '%o'; + } else { + prefix += '%s'; + } + }); + let processedArgs = [prefix, `color: ${color}`, `color: ${bodyColor}`]; + processedArgs = processedArgs.concat(argsArray); + return processedArgs; +}; +const parseLogConf = (logConf: string, options: Options): { level: string; bizName: string} => { + if (!logConf) { + return { + level: options.level, + bizName: options.bizName, + }; + } + if (logConf.indexOf(':') > -1) { + const pair = logConf.split(':'); + return { + level: pair[0], + bizName: pair[1] || '*', + }; + } + return { + level: logConf, + bizName: '*', + }; +}; + +const defaultOptions: Options = { + level: 'warn', + bizName: '*', +}; + +class Logger { + bizName: string; + targetBizName: string; + targetLevel: string; + constructor(options: Options) { + options = { ...defaultOptions, ...options }; + const _location = location || {} as any; + // __logConf__ 格式为 logLevel[:bizName], bizName is used as: targetBizName like '%bizName%' + // 1. __logConf__=log or __logConf__=warn, etc. + // 2. __logConf__=log:* or __logConf__=warn:*, etc. + // 2. __logConf__=log:bizName or __logConf__=warn:partOfBizName, etc. + const logConf = (((/__(?:logConf|logLevel)__=([^#/&]*)/.exec(_location.href)) || [])[1]); + const targetOptions = parseLogConf(logConf, options); + this.bizName = options.bizName; + this.targetBizName = targetOptions.bizName; + this.targetLevel = targetOptions.level; + } + debug(...args: any[]): void { + if (!shouldOutput('debug', this.targetLevel, this.bizName, this.targetBizName)) { + return; + } + return output('debug', this.bizName)(args); + } + log(...args: any[]): void { + if (!shouldOutput('log', this.targetLevel, this.bizName, this.targetBizName)) { + return; + } + return output('log', this.bizName)(args); + } + info(...args: any[]): void { + if (!shouldOutput('info', this.targetLevel, this.bizName, this.targetBizName)) { + return; + } + return output('info', this.bizName)(args); + } + warn(...args: any[]): void { + if (!shouldOutput('warn', this.targetLevel, this.bizName, this.targetBizName)) { + return; + } + return output('warn', this.bizName)(args); + } + error(...args: any[]): void { + if (!shouldOutput('error', this.targetLevel, this.bizName, this.targetBizName)) { + return; + } + return output('error', this.bizName)(args); + } +} export { Logger }; diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index 87eea62f82..28833ef321 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -1,8 +1,11 @@ import { isI18NObject } from './is-object'; import { get } from 'lodash'; -import { ComponentMeta } from '@alilc/lowcode-designer'; -import { TransformStage } from '@alilc/lowcode-types'; +import { IPublicEnumTransformStage, IPublicModelComponentMeta } from '@alilc/lowcode-types'; +import { Logger } from './logger'; + +const logger = new Logger({ level: 'warn', bizName: 'utils' }); + interface Variable { type: 'variable'; variable: string; @@ -10,7 +13,10 @@ interface Variable { } export function isVariable(obj: any): obj is Variable { - return obj && obj.type === 'variable'; + if (!obj || typeof obj !== 'object') { + return false; + } + return obj.type === 'variable'; } export function isUseI18NSetter(prototype: any, propName: string) { @@ -23,7 +29,7 @@ export function isUseI18NSetter(prototype: any, propName: string) { return false; } -export function convertToI18NObject(v: string | any, locale: string = 'zh_CN') { +export function convertToI18NObject(v: string | any, locale: string = 'zh-CN') { if (isI18NObject(v)) return v; return { type: 'i18n', use: locale, [locale]: v }; } @@ -65,7 +71,7 @@ export function arrShallowEquals(arr1: any[], arr2: any[]): boolean { * 判断当前 meta 是否从 vc prototype 转换而来 * @param meta */ - export function isFromVC(meta: ComponentMeta) { + export function isFromVC(meta: IPublicModelComponentMeta) { return !!meta?.getMetadata().configure?.advanced; } @@ -81,17 +87,18 @@ const stageList = [ 'init', 'upgrade', ]; + /** * 兼容原来的数字版本的枚举对象 * @param stage * @returns */ -export function compatStage(stage: TransformStage | number): TransformStage { +export function compatStage(stage: IPublicEnumTransformStage | number): IPublicEnumTransformStage { if (typeof stage === 'number') { - console.warn('stage 直接指定为数字的使用方式已经过时,将在下一版本移除,请直接使用 TransformStage.Render|Serilize|Save|Clone|Init|Upgrade'); - return stageList[stage - 1] as TransformStage; + console.warn('stage 直接指定为数字的使用方式已经过时,将在下一版本移除,请直接使用 IPublicEnumTransformStage.Render|Serilize|Save|Clone|Init|Upgrade'); + return stageList[stage - 1] as IPublicEnumTransformStage; } - return stage as TransformStage; + return stage as IPublicEnumTransformStage; } export function invariant(check: any, message: string, thing?: any) { @@ -102,10 +109,27 @@ export function invariant(check: any, message: string, thing?: any) { export function deprecate(fail: any, message: string, alterative?: string) { if (fail) { - console.warn(`Deprecation: ${message}` + (alterative ? `, use ${alterative} instead.` : '')); + logger.warn(`Deprecation: ${message}` + (alterative ? `, use ${alterative} instead.` : '')); } } export function isRegExp(obj: any): obj is RegExp { - return obj && obj.test && obj.exec && obj.compile; + if (!obj || typeof obj !== 'object') { + return false; + } + return 'test' in obj && 'exec' in obj && 'compile' in obj; +} + +/** + * The prop supportVariable SHOULD take precedence over default global supportVariable. + * @param propSupportVariable prop supportVariable + * @param globalSupportVariable global supportVariable + * @returns + */ +export function shouldUseVariableSetter( + propSupportVariable: boolean | undefined, + globalSupportVariable: boolean, +) { + if (propSupportVariable === false) return false; + return propSupportVariable || globalSupportVariable; } \ No newline at end of file diff --git a/packages/utils/src/navtive-selection.ts b/packages/utils/src/navtive-selection.ts index 76f51f48aa..b8e5257734 100644 --- a/packages/utils/src/navtive-selection.ts +++ b/packages/utils/src/navtive-selection.ts @@ -1,4 +1,5 @@ -let nativeSelectionEnabled = true; +export let nativeSelectionEnabled = true; + const preventSelection = (e: Event) => { if (nativeSelectionEnabled) { return null; diff --git a/packages/utils/src/node-helper.ts b/packages/utils/src/node-helper.ts index 5f05472744..60102d6794 100644 --- a/packages/utils/src/node-helper.ts +++ b/packages/utils/src/node-helper.ts @@ -1,7 +1,11 @@ // 仅使用类型 -import { Node } from '@alilc/lowcode-designer'; +import { IPublicModelNode } from '@alilc/lowcode-types'; +import { MouseEvent } from 'react'; -export const getClosestNode = (node: Node, until: (node: Node) => boolean): Node | undefined => { +export const getClosestNode = <Node extends IPublicModelNode = IPublicModelNode>( + node: Node, + until: (n: Node) => boolean, + ): Node | undefined => { if (!node) { return undefined; } @@ -9,7 +13,7 @@ export const getClosestNode = (node: Node, until: (node: Node) => boolean): Node return node; } else { // @ts-ignore - return getClosestNode(node.getParent(), until); + return getClosestNode(node.parent, until); } }; @@ -19,8 +23,8 @@ export const getClosestNode = (node: Node, until: (node: Node) => boolean): Node * @param {unknown} e 点击事件 * @returns {boolean} 是否可点击,true表示可点击 */ -export const canClickNode = (node: Node, e: unknown): boolean => { - const onClickHook = node.componentMeta?.getMetadata().configure?.advanced?.callbacks?.onClickHook; - const canClick = typeof onClickHook === 'function' ? onClickHook(e as MouseEvent, node) : true; +export function canClickNode<Node extends IPublicModelNode = IPublicModelNode>(node: Node, e: MouseEvent): boolean { + const onClickHook = node.componentMeta?.advanced?.callbacks?.onClickHook; + const canClick = typeof onClickHook === 'function' ? onClickHook(e, node) : true; return canClick; -}; +} diff --git a/packages/utils/src/schema.ts b/packages/utils/src/schema.ts index 1580003d29..2e7dec70fa 100644 --- a/packages/utils/src/schema.ts +++ b/packages/utils/src/schema.ts @@ -1,4 +1,5 @@ -import { isJSBlock, isJSSlot, ActivityType, NodeSchema, PageSchema, RootSchema } from '@alilc/lowcode-types'; +import { ActivityType, IPublicTypeNodeSchema, IPublicTypeRootSchema } from '@alilc/lowcode-types'; +import { isJSBlock, isJSSlot } from './check-types'; import { isVariable } from './misc'; import { isPlainObject } from './is-plain-object'; @@ -77,8 +78,8 @@ export function compatibleLegaoSchema(props: any): any { return newProps; } -export function getNodeSchemaById(schema: NodeSchema, nodeId: string): NodeSchema | undefined { - let found: NodeSchema | undefined; +export function getNodeSchemaById(schema: IPublicTypeNodeSchema, nodeId: string): IPublicTypeNodeSchema | undefined { + let found: IPublicTypeNodeSchema | undefined; if (schema.id === nodeId) { return schema; } @@ -86,7 +87,7 @@ export function getNodeSchemaById(schema: NodeSchema, nodeId: string): NodeSchem // 查找 children if (Array.isArray(children)) { for (const child of children) { - found = getNodeSchemaById(child as NodeSchema, nodeId); + found = getNodeSchemaById(child as IPublicTypeNodeSchema, nodeId); if (found) return found; } } @@ -97,19 +98,19 @@ export function getNodeSchemaById(schema: NodeSchema, nodeId: string): NodeSchem } } -function getNodeSchemaFromPropsById(props: any, nodeId: string): NodeSchema | undefined { - let found: NodeSchema | undefined; - for (const [key, value] of Object.entries(props)) { +function getNodeSchemaFromPropsById(props: any, nodeId: string): IPublicTypeNodeSchema | undefined { + let found: IPublicTypeNodeSchema | undefined; + for (const [_key, value] of Object.entries(props)) { if (isJSSlot(value)) { - // value 是数组类型 { type: 'JSSlot', value: NodeSchema[] } + // value 是数组类型 { type: 'JSSlot', value: IPublicTypeNodeSchema[] } if (Array.isArray(value.value)) { for (const child of value.value) { - found = getNodeSchemaById(child as NodeSchema, nodeId); + found = getNodeSchemaById(child as IPublicTypeNodeSchema, nodeId); if (found) return found; } } - // value 是对象类型 { type: 'JSSlot', value: NodeSchema } - found = getNodeSchemaById(value.value as NodeSchema, nodeId); + // value 是对象类型 { type: 'JSSlot', value: IPublicTypeNodeSchema } + found = getNodeSchemaById(value.value as IPublicTypeNodeSchema, nodeId); if (found) return found; } else if (isPlainObject(value)) { found = getNodeSchemaFromPropsById(value, nodeId); @@ -118,12 +119,16 @@ function getNodeSchemaFromPropsById(props: any, nodeId: string): NodeSchema | un } } -export function applyActivities(pivotSchema: RootSchema, activities: any, options?: any): RootSchema { +/** + * TODO: not sure if this is used anywhere + * @deprecated + */ +export function applyActivities(pivotSchema: IPublicTypeRootSchema, activities: any): IPublicTypeRootSchema { let schema = { ...pivotSchema }; if (!Array.isArray(activities)) { activities = [activities]; } - return activities.reduce((accSchema: RootSchema, activity: any) => { + return activities.reduce((accSchema: IPublicTypeRootSchema, activity: any) => { if (activity.type === ActivityType.MODIFIED) { const found = getNodeSchemaById(accSchema, activity.payload.schema.id); if (!found) return accSchema; diff --git a/packages/utils/src/script.ts b/packages/utils/src/script.ts index 7c772f03f9..c4c476fac4 100644 --- a/packages/utils/src/script.ts +++ b/packages/utils/src/script.ts @@ -1,14 +1,18 @@ import { createDefer } from './create-defer'; +import { Logger } from './logger'; -export function evaluate(script: string) { +const logger = new Logger({ level: 'warn', bizName: 'utils' }); + +export function evaluate(script: string, scriptType?: string) { const scriptEl = document.createElement('script'); + scriptType && (scriptEl.type = scriptType); scriptEl.text = script; document.head.appendChild(scriptEl); document.head.removeChild(scriptEl); } -export function load(url: string) { - const node: any = document.createElement('script'); +export function load(url: string, scriptType?: string) { + const node = document.createElement('script'); // node.setAttribute('crossorigin', 'anonymous'); @@ -34,6 +38,8 @@ export function load(url: string) { // `async=false` is required to make sure all js resources execute sequentially. node.async = false; + scriptType && (node.type = scriptType); + document.head.appendChild(node); return i.promise(); @@ -50,7 +56,7 @@ export function newFunction(args: string, code: string) { // eslint-disable-next-line no-new-func return new Function(args, code); } catch (e) { - console.warn('Caught error, Cant init func'); + logger.warn('Caught error, Cant init func'); return null; } } diff --git a/packages/utils/src/svg-icon.tsx b/packages/utils/src/svg-icon.tsx index f75724b064..2513f7bcab 100644 --- a/packages/utils/src/svg-icon.tsx +++ b/packages/utils/src/svg-icon.tsx @@ -1,4 +1,4 @@ -import { ReactNode } from 'react'; +import React, { ReactNode } from 'react'; const SizePresets: any = { xsmall: 8, diff --git a/packages/utils/src/transaction-manager.ts b/packages/utils/src/transaction-manager.ts new file mode 100644 index 0000000000..85161eff9f --- /dev/null +++ b/packages/utils/src/transaction-manager.ts @@ -0,0 +1,29 @@ +import { IPublicEnumTransitionType } from '@alilc/lowcode-types'; +import { runInAction } from 'mobx'; +import EventEmitter from 'events'; + +class TransactionManager { + emitter = new EventEmitter(); + + executeTransaction = (fn: () => void, type: IPublicEnumTransitionType = IPublicEnumTransitionType.REPAINT): void => { + this.emitter.emit(`[${type}]startTransaction`); + runInAction(fn); + this.emitter.emit(`[${type}]endTransaction`); + }; + + onStartTransaction = (fn: () => void, type: IPublicEnumTransitionType = IPublicEnumTransitionType.REPAINT): () => void => { + this.emitter.on(`[${type}]startTransaction`, fn); + return () => { + this.emitter.off(`[${type}]startTransaction`, fn); + }; + }; + + onEndTransaction = (fn: () => void, type: IPublicEnumTransitionType = IPublicEnumTransitionType.REPAINT): () => void => { + this.emitter.on(`[${type}]endTransaction`, fn); + return () => { + this.emitter.off(`[${type}]endTransaction`, fn); + }; + }; +} + +export const transactionManager = new TransactionManager(); diff --git a/packages/utils/src/workspace.tsx b/packages/utils/src/workspace.tsx new file mode 100644 index 0000000000..446530ce8e --- /dev/null +++ b/packages/utils/src/workspace.tsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { IPublicModelPluginContext, IPublicEnumPluginRegisterLevel, IPublicModelWindow, IPublicModelEditorView } from '@alilc/lowcode-types'; + +/** + * 高阶组件(HOC):为组件提供 view 插件上下文。 + * + * @param {React.ComponentType} Component - 需要被封装的组件。 + * @param {string|string[]} viewName - 视图名称或视图名称数组,用于过滤特定的视图插件上下文。 + * @returns {React.ComponentType} 返回封装后的组件。 + * + * @example + * // 用法示例(函数组件): + * const EnhancedComponent = ProvideViewPluginContext(MyComponent, "viewName"); + */ +export const ProvideViewPluginContext = (Component: any, viewName?: string | string[]) => { + // 创建一个新的函数组件,以便在其中使用 Hooks + return function WithPluginContext(props: { + [key: string]: any; + + pluginContext?: IPublicModelPluginContext; + }) { + const getPluginContextFun = useCallback((editorWindow?: IPublicModelWindow | null) => { + if (!editorWindow?.currentEditorView) { + return null; + } + if (viewName) { + const items = editorWindow?.editorViews.filter(d => (d as any).viewName === viewName || (Array.isArray(viewName) && viewName.includes((d as any).viewName))); + return items[0]; + } else { + return editorWindow.currentEditorView; + } + }, []); + + const { workspace } = props.pluginContext || {}; + const [pluginContext, setPluginContext] = useState<IPublicModelEditorView | null>(getPluginContextFun(workspace?.window)); + + useEffect(() => { + if (workspace?.window) { + const ctx = getPluginContextFun(workspace.window); + ctx && setPluginContext(ctx); + } + return workspace?.onChangeActiveEditorView(() => { + const ctx = getPluginContextFun(workspace.window); + ctx && setPluginContext(ctx); + }); + }, [workspace, getPluginContextFun]); + + if (props.pluginContext?.registerLevel !== IPublicEnumPluginRegisterLevel.Workspace || !props.pluginContext) { + return <Component {...props} />; + } + + return <Component {...props} pluginContext={pluginContext} />; + }; +}; diff --git a/packages/utils/test/src/__snapshots__/is-react.test.tsx.snap b/packages/utils/test/src/__snapshots__/is-react.test.tsx.snap new file mode 100644 index 0000000000..14ef394533 --- /dev/null +++ b/packages/utils/test/src/__snapshots__/is-react.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`wrapReactClass should render the FunctionComponent with props 1`] = ` +<FunctionComponent + prop1="value1" + prop2="value2" +> + Child Text +</FunctionComponent> +`; diff --git a/packages/utils/test/src/build-components/buildComponents.test.tsx b/packages/utils/test/src/build-components/buildComponents.test.tsx new file mode 100644 index 0000000000..a50a68b396 --- /dev/null +++ b/packages/utils/test/src/build-components/buildComponents.test.tsx @@ -0,0 +1,616 @@ +import React from 'react'; +import { + accessLibrary, + generateHtmlComp, + getSubComponent, + buildComponents, + getProjectUtils, +} from "../../../src/build-components"; + +function Button() {}; + +function WrapButton() {}; + +function ButtonGroup() {}; + +function WrapButtonGroup() {}; + +ButtonGroup.Button = Button; + +Button.displayName = "Button"; +ButtonGroup.displayName = "ButtonGroup"; +ButtonGroup.prototype.isReactComponent = true; +Button.prototype.isReactComponent = true; + +jest.mock('../../../src/is-react', () => { + const original = jest.requireActual('../../../src/is-react'); + return { + ...original, + wrapReactClass(view) { + return view; + } + } +}); + +describe('accessLibrary', () => { + it('should return a library object when given a library object', () => { + const libraryObject = { key: 'value' }; + const result = accessLibrary(libraryObject); + expect(result).toEqual(libraryObject); + }); + + it('should generate an HTML component when given a string library name', () => { + const libraryName = 'div'; + const result = accessLibrary(libraryName); + + // You can write more specific assertions to validate the generated component + expect(result).toBeDefined(); + }); + + // Add more test cases to cover other scenarios +}); + +describe('generateHtmlComp', () => { + it('should generate an HTML component for valid HTML tags', () => { + const htmlTags = ['a', 'img', 'div', 'span', 'svg']; + htmlTags.forEach((tag) => { + const result = generateHtmlComp(tag); + + // You can write more specific assertions to validate the generated component + expect(result).toBeDefined(); + }); + }); + + it('should return undefined for an invalid HTML tag', () => { + const invalidTag = 'invalidtag'; + const result = generateHtmlComp(invalidTag); + expect(result).toBeUndefined(); + }); + + // Add more test cases to cover other scenarios +}); + +describe('getSubComponent', () => { + it('should return the root library if paths are empty', () => { + const library = { component: 'RootComponent' }; + const paths = []; + const result = getSubComponent(library, paths); + expect(result).toEqual(library); + }); + + it('should return the specified sub-component', () => { + const library = { + components: { + Button: 'ButtonComponent', + Text: 'TextComponent', + }, + }; + const paths = ['components', 'Button']; + const result = getSubComponent(library, paths); + expect(result).toEqual('ButtonComponent'); + }); + + it('should handle missing keys in the path', () => { + const library = { + components: { + Button: 'ButtonComponent', + }, + }; + const paths = ['components', 'Text']; + const result = getSubComponent(library, paths); + expect(result).toEqual({ + Button: 'ButtonComponent', + }); + }); + + it('should handle exceptions and return null', () => { + const library = 'ButtonComponent'; + const paths = ['components', 'Button']; + // Simulate an exception by providing a non-object in place of 'ButtonComponent' + const result = getSubComponent(library, paths); + expect(result).toBeNull(); + }); + + it('should handle the "default" key as the first path element', () => { + const library = { + default: 'DefaultComponent', + }; + const paths = ['default']; + const result = getSubComponent(library, paths); + expect(result).toEqual('DefaultComponent'); + }); +}); + +describe('getProjectUtils', () => { + it('should return an empty object when given empty metadata and library map', () => { + const libraryMap = {}; + const utilsMetadata = []; + const result = getProjectUtils(libraryMap, utilsMetadata); + expect(result).toEqual({}); + }); + + it('should return project utilities based on metadata and library map', () => { + const libraryMap = { + 'package1': 'library1', + 'package2': 'library2', + }; + + const utilsMetadata = [ + { + name: 'util1', + npm: { + package: 'package1', + }, + }, + { + name: 'util2', + npm: { + package: 'package2', + }, + }, + ]; + + global['library1'] = { name: 'library1' }; + global['library2'] = { name: 'library2' }; + + const result = getProjectUtils(libraryMap, utilsMetadata); + + // Define the expected output based on the mocked accessLibrary + const expectedOutput = { + 'util1': { name: 'library1' }, + 'util2': { name: 'library2' }, + }; + + expect(result).toEqual(expectedOutput); + + global['library1'] = null; + global['library1'] = null; + }); + + it('should handle metadata with destructuring', () => { + const libraryMap = { + 'package1': { destructuring: true, util1: 'library1', util2: 'library2' }, + }; + + const utilsMetadata = [ + { + name: 'util1', + npm: { + package: 'package1', + destructuring: true, + }, + }, + ]; + + const result = getProjectUtils(libraryMap, utilsMetadata); + + // Define the expected output based on the mocked accessLibrary + const expectedOutput = { + 'util1': 'library1', + 'util2': 'library2', + }; + + expect(result).toEqual(expectedOutput); + }); +}); + +describe('buildComponents', () => { + it('should create components from component map with React components', () => { + const libraryMap = {}; + const componentsMap = { + Button: () => <button>Button</button>, + Text: () => <p>Text</p>, + }; + + const createComponent = (schema) => { + // Mock createComponent function + return schema.componentsTree.map((component) => component.component); + }; + + const result = buildComponents(libraryMap, componentsMap, createComponent); + + expect(result.Button).toBeDefined(); + expect(result.Text).toBeDefined(); + }); + + it('should create components from component map with component schemas', () => { + const libraryMap = {}; + const componentsMap = { + Button: { + componentsTree: [ + { + componentName: 'Component' + } + ] + }, + Text: { + componentsTree: [ + { + componentName: 'Component' + } + ] + }, + }; + + const createComponent = (schema) => { + // Mock createComponent function + return schema.componentsTree.map((component) => component.component); + }; + + const result = buildComponents(libraryMap, componentsMap, createComponent); + + expect(result.Button).toBeDefined(); + expect(result.Text).toBeDefined(); + }); + + it('should create components from component map with React components and schemas', () => { + const libraryMap = {}; + const componentsMap = { + Button: () => <button>Button</button>, + Text: { + type: 'ComponentSchema', + // Add component schema properties here + }, + }; + + const createComponent = (schema) => { + // Mock createComponent function + return schema.componentsTree.map((component) => component.component); + }; + + const result = buildComponents(libraryMap, componentsMap, createComponent); + + expect(result.Button).toBeDefined(); + expect(result.Text).toBeDefined(); + }); + + it('should create components from component map with library mappings', () => { + const libraryMap = { + 'libraryName1': 'library1', + 'libraryName2': 'library2', + }; + const componentsMap = { + Button: { + package: 'libraryName1', + version: '1.0', + exportName: 'ButtonComponent', + }, + Text: { + package: 'libraryName2', + version: '2.0', + exportName: 'TextComponent', + }, + }; + + const createComponent = (schema) => { + // Mock createComponent function + return schema.componentsTree.map((component) => component.component); + }; + + global['library1'] = () => <button>ButtonComponent</button>; + global['library2'] = () => () => <p>TextComponent</p>; + + const result = buildComponents(libraryMap, componentsMap, createComponent); + + expect(result.Button).toBeDefined(); + expect(result.Text).toBeDefined(); + + global['library1'] = null; + global['library2'] = null; + }); +}); + +describe('build-component', () => { + it('basic button', () => { + expect( + buildComponents( + { + '@alilc/button': { + Button, + } + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + destructuring: true, + exportName: 'Button', + subName: 'Button', + } + }, + () => {}, + )) + .toEqual({ + Button, + }); + }); + + it('component is a __esModule', () => { + expect( + buildComponents( + { + '@alilc/button': { + __esModule: true, + default: Button, + } + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + } + }, + () => {}, + )) + .toEqual({ + Button, + }); + }) + + it('basic warp button', () => { + expect( + buildComponents( + { + '@alilc/button': { + WrapButton, + } + }, + { + WrapButton: { + componentName: 'WrapButton', + package: '@alilc/button', + destructuring: true, + exportName: 'WrapButton', + subName: 'WrapButton', + } + }, + () => {}, + )) + .toEqual({ + WrapButton, + }); + }); + + it('destructuring is false button', () => { + expect( + buildComponents( + { + '@alilc/button': Button + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + destructuring: false, + } + }, + () => {}, + )) + .toEqual({ + Button, + }); + }); + + it('Button and ButtonGroup', () => { + expect( + buildComponents( + { + '@alilc/button': { + Button, + ButtonGroup, + } + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + destructuring: true, + exportName: 'Button', + subName: 'Button', + }, + ButtonGroup: { + componentName: 'ButtonGroup', + package: '@alilc/button', + destructuring: true, + exportName: 'ButtonGroup', + subName: 'ButtonGroup', + } + }, + () => {}, + )) + .toEqual({ + Button, + ButtonGroup, + }); + }); + + it('ButtonGroup and ButtonGroup.Button', () => { + expect( + buildComponents( + { + '@alilc/button': { + ButtonGroup, + } + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + destructuring: true, + exportName: 'ButtonGroup', + subName: 'ButtonGroup.Button', + }, + ButtonGroup: { + componentName: 'ButtonGroup', + package: '@alilc/button', + destructuring: true, + exportName: 'ButtonGroup', + subName: 'ButtonGroup', + } + }, + () => {}, + )) + .toEqual({ + Button, + ButtonGroup, + }); + }); + + it('ButtonGroup.default and ButtonGroup.Button', () => { + expect( + buildComponents( + { + '@alilc/button': ButtonGroup, + }, + { + Button: { + componentName: 'Button', + package: '@alilc/button', + destructuring: true, + exportName: 'Button', + subName: 'Button', + }, + ButtonGroup: { + componentName: 'ButtonGroup', + package: '@alilc/button', + destructuring: true, + exportName: 'default', + subName: 'default', + } + }, + () => {}, + )) + .toEqual({ + Button, + ButtonGroup, + }); + }); + + it('no npm component', () => { + expect( + buildComponents( + { + '@alilc/button': Button, + }, + { + Button: null, + }, + () => {}, + )) + .toEqual({}); + }); + + it('no npm component and global button', () => { + window.Button = Button; + expect( + buildComponents( + {}, + { + Button: null, + }, + () => {}, + )) + .toEqual({ + Button, + }); + window.Button = null; + }); + + it('componentsMap value is component funtion', () => { + expect( + buildComponents( + {}, + { + Button, + }, + () => {}, + )) + .toEqual({ + Button, + }); + }); + + + it('componentsMap value is component', () => { + expect( + buildComponents( + {}, + { + Button: WrapButton, + }, + () => {}, + )) + .toEqual({ + Button: WrapButton, + }); + }); + + it('componentsMap value is mix component', () => { + expect( + buildComponents( + {}, + { + Button: { + WrapButton, + Button, + ButtonGroup, + }, + }, + () => {}, + )) + .toEqual({ + Button: { + WrapButton, + Button, + ButtonGroup, + }, + }); + }); + + it('componentsMap value is Lowcode Component', () => { + expect( + buildComponents( + {}, + { + Button: { + componentName: 'Component', + schema: {}, + }, + }, + (component) => { + return component as any; + }, + )) + .toEqual({ + Button: { + componentsMap: [], + componentsTree: [ + { + componentName: 'Component', + schema: {}, + } + ], + version: "", + }, + }); + }) +}); + +describe('build div component', () => { + it('build div component', () => { + const components = buildComponents( + { + '@alilc/div': 'div' + }, + { + div: { + componentName: 'div', + package: '@alilc/div' + } + }, + () => {}, + ); + + expect(components['div']).not.toBeNull(); + }) +}) \ No newline at end of file diff --git a/packages/utils/test/src/build-components/getProjectUtils.test.ts b/packages/utils/test/src/build-components/getProjectUtils.test.ts new file mode 100644 index 0000000000..216f3db427 --- /dev/null +++ b/packages/utils/test/src/build-components/getProjectUtils.test.ts @@ -0,0 +1,43 @@ +import { getProjectUtils } from "../../../src/build-components"; + +const sampleUtil = () => 'I am a sample util'; +const sampleUtil2 = () => 'I am a sample util 2'; + +describe('get project utils', () => { + it('get utils with destructuring true', () => { + expect(getProjectUtils( + { + '@alilc/utils': { + destructuring: true, + sampleUtil, + sampleUtil2, + } + }, + [{ + name: 'sampleUtils', + npm: { + package: '@alilc/utils' + } + }] + )).toEqual({ + sampleUtil, + sampleUtil2, + }) + }); + + it('get utils with name', () => { + expect(getProjectUtils( + { + '@alilc/utils': sampleUtil + }, + [{ + name: 'sampleUtil', + npm: { + package: '@alilc/utils' + } + }] + )).toEqual({ + sampleUtil, + }) + }); +}) \ No newline at end of file diff --git a/packages/utils/test/src/build-components/getSubComponent.test.ts b/packages/utils/test/src/build-components/getSubComponent.test.ts new file mode 100644 index 0000000000..ca91bb2304 --- /dev/null +++ b/packages/utils/test/src/build-components/getSubComponent.test.ts @@ -0,0 +1,85 @@ +import { getSubComponent } from '../../../src/build-components'; + +function Button() {} + +function ButtonGroup() {} + +ButtonGroup.Button = Button; + +function OnlyButtonGroup() {} + +describe('getSubComponent library is object', () => { + it('get Button from Button', () => { + expect(getSubComponent({ + Button, + }, ['Button'])).toBe(Button); + }); + + it('get ButtonGroup.Button from ButtonGroup', () => { + expect(getSubComponent({ + ButtonGroup, + }, ['ButtonGroup', 'Button'])).toBe(Button); + }); + + it('get ButtonGroup from ButtonGroup', () => { + expect(getSubComponent({ + ButtonGroup, + }, ['ButtonGroup'])).toBe(ButtonGroup); + }); + + it('get ButtonGroup.Button from OnlyButtonGroup', () => { + expect(getSubComponent({ + ButtonGroup: OnlyButtonGroup, + }, ['ButtonGroup', 'Button'])).toBe(OnlyButtonGroup); + }); +}); + +describe('getSubComponent library is null', () => { + it('getSubComponent library is null', () => { + expect(getSubComponent(null, ['ButtonGroup', 'Button'])).toBeNull(); + }); +}) + +describe('getSubComponent paths is []', () => { + it('getSubComponent paths is []', () => { + expect(getSubComponent(Button, [])).toBe(Button); + }); +}); + +describe('getSubComponent make error', () => { + it('library is string', () => { + expect(getSubComponent(true, ['Button'])).toBe(null); + }); + + it('library is boolean', () => { + expect(getSubComponent('I am a string', ['Button'])).toBe(null); + }); + + it('library is number', () => { + expect(getSubComponent(123, ['Button'])).toBe(null); + }); + + it('library ButtonGroup is null', () => { + expect(getSubComponent({ + ButtonGroup: null, + }, ['ButtonGroup', 'Button'])).toBe(null); + }); + + it('library ButtonGroup.Button is null', () => { + expect(getSubComponent({ + ButtonGroup: null, + }, ['ButtonGroup', 'Button', 'SubButton'])).toBe(null); + }); + + it('path s is [[]]', () => { + expect(getSubComponent({ + ButtonGroup: null, + }, [['ButtonGroup'] as any, 'Button'])).toBe(null); + }); + + it('ButtonGroup is undefined', () => { + expect(getSubComponent({ + ButtonGroup: undefined, + }, ['ButtonGroup', 'Button'])).toBe(null); + }); +}) \ 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-action-content-object.test.ts b/packages/utils/test/src/check-types/is-action-content-object.test.ts new file mode 100644 index 0000000000..08b95788d1 --- /dev/null +++ b/packages/utils/test/src/check-types/is-action-content-object.test.ts @@ -0,0 +1,20 @@ +import { isActionContentObject } from '../../../src/check-types/is-action-content-object'; + +describe('isActionContentObject', () => { + test('should return true for an object', () => { + const obj = { prop: 'value' }; + expect(isActionContentObject(obj)).toBe(true); + }); + + test('should return false for a non-object', () => { + expect(isActionContentObject('not an object')).toBe(false); + expect(isActionContentObject(123)).toBe(false); + expect(isActionContentObject(null)).toBe(false); + expect(isActionContentObject(undefined)).toBe(false); + }); + + test('should return false for an empty object', () => { + const obj = {}; + expect(isActionContentObject(obj)).toBe(true); + }); +}); 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-custom-view.test.tsx b/packages/utils/test/src/check-types/is-custom-view.test.tsx new file mode 100644 index 0000000000..62c08780e6 --- /dev/null +++ b/packages/utils/test/src/check-types/is-custom-view.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { isCustomView } from '../../../src/check-types/is-custom-view'; +import { IPublicTypeCustomView } from '@alilc/lowcode-types'; + +describe('isCustomView', () => { + test('should return true when obj is a valid React element', () => { + const obj: IPublicTypeCustomView = <div>Hello, World!</div>; + expect(isCustomView(obj)).toBe(true); + }); + + test('should return true when obj is a valid React component', () => { + const MyComponent: React.FC = () => <div>Hello, World!</div>; + const obj: IPublicTypeCustomView = MyComponent; + expect(isCustomView(obj)).toBe(true); + }); + + test('should return false when obj is null or undefined', () => { + expect(isCustomView(null)).toBe(false); + expect(isCustomView(undefined)).toBe(false); + }); + + test('should return false when obj is not a valid React element or component', () => { + const obj: IPublicTypeCustomView = 'not a valid object'; + expect(isCustomView(obj)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-dom-text.test.ts b/packages/utils/test/src/check-types/is-dom-text.test.ts new file mode 100644 index 0000000000..50dce0fb7a --- /dev/null +++ b/packages/utils/test/src/check-types/is-dom-text.test.ts @@ -0,0 +1,13 @@ +import { isDOMText } from '../../../src/check-types/is-dom-text'; + +describe('isDOMText', () => { + it('should return true when the input is a string', () => { + const result = isDOMText('Hello World'); + expect(result).toBe(true); + }); + + it('should return false when the input is not a string', () => { + const result = isDOMText(123); + expect(result).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-drag-any-object.test.ts b/packages/utils/test/src/check-types/is-drag-any-object.test.ts new file mode 100644 index 0000000000..6a835f2be3 --- /dev/null +++ b/packages/utils/test/src/check-types/is-drag-any-object.test.ts @@ -0,0 +1,32 @@ +import { isDragAnyObject } from '../../../src/check-types/is-drag-any-object'; +import { IPublicEnumDragObjectType } from '@alilc/lowcode-types'; + +describe('isDragAnyObject', () => { + it('should return false if obj is null', () => { + const result = isDragAnyObject(null); + expect(result).toBe(false); + }); + + it('should return false if obj is number', () => { + const result = isDragAnyObject(2); + expect(result).toBe(false); + }); + + it('should return false if obj.type is NodeData', () => { + const obj = { type: IPublicEnumDragObjectType.NodeData }; + const result = isDragAnyObject(obj); + expect(result).toBe(false); + }); + + it('should return false if obj.type is Node', () => { + const obj = { type: IPublicEnumDragObjectType.Node }; + const result = isDragAnyObject(obj); + expect(result).toBe(false); + }); + + it('should return true if obj.type is anything else', () => { + const obj = { type: 'SomeOtherType' }; + const result = isDragAnyObject(obj); + expect(result).toBe(true); + }); +}); diff --git a/packages/utils/test/src/check-types/is-drag-node-data-object.test.ts b/packages/utils/test/src/check-types/is-drag-node-data-object.test.ts new file mode 100644 index 0000000000..92867843a2 --- /dev/null +++ b/packages/utils/test/src/check-types/is-drag-node-data-object.test.ts @@ -0,0 +1,29 @@ +import { IPublicEnumDragObjectType, IPublicTypeDragNodeDataObject } from '@alilc/lowcode-types'; +import { isDragNodeDataObject } from '../../../src/check-types/is-drag-node-data-object'; + +describe('isDragNodeDataObject', () => { + test('should return true for valid IPublicTypeDragNodeDataObject', () => { + const obj: IPublicTypeDragNodeDataObject = { + type: IPublicEnumDragObjectType.NodeData, + // 其他属性... + }; + + expect(isDragNodeDataObject(obj)).toBe(true); + }); + + test('should return false for invalid IPublicTypeDragNodeDataObject', () => { + const obj: any = { + type: 'InvalidType', + // 其他属性... + }; + + expect(isDragNodeDataObject(obj)).toBe(false); + }); + + test('should return false for null or undefined', () => { + expect(isDragNodeDataObject(null)).toBe(false); + expect(isDragNodeDataObject(undefined)).toBe(false); + }); + + // 可以添加更多测试用例... +}); diff --git a/packages/utils/test/src/check-types/is-drag-node-object.test.ts b/packages/utils/test/src/check-types/is-drag-node-object.test.ts new file mode 100644 index 0000000000..3561c87885 --- /dev/null +++ b/packages/utils/test/src/check-types/is-drag-node-object.test.ts @@ -0,0 +1,36 @@ +import { IPublicEnumDragObjectType } from '@alilc/lowcode-types'; +import { isDragNodeObject } from '../../../src/check-types/is-drag-node-object'; + +describe('isDragNodeObject', () => { + it('should return true if the object is of IPublicTypeDragNodeObject type and has type IPublicEnumDragObjectType.Node', () => { + const obj = { + type: IPublicEnumDragObjectType.Node, + //... other properties + }; + + expect(isDragNodeObject(obj)).toBe(true); + }); + + it('should return false if the object is not of IPublicTypeDragNodeObject type', () => { + const obj = { + type: IPublicEnumDragObjectType.OtherType, + //... other properties + }; + + expect(isDragNodeObject(obj)).toBe(false); + }); + + it('should return false if the object is of IPublicTypeDragNodeObject type but type is not IPublicEnumDragObjectType.Node', () => { + const obj = { + type: IPublicEnumDragObjectType.OtherType, + //... other properties + }; + + expect(isDragNodeObject(obj)).toBe(false); + }); + + it('should return false if the object is null or undefined', () => { + expect(isDragNodeObject(null)).toBe(false); + expect(isDragNodeObject(undefined)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-dynamic-setter.test.ts b/packages/utils/test/src/check-types/is-dynamic-setter.test.ts new file mode 100644 index 0000000000..72f55367d0 --- /dev/null +++ b/packages/utils/test/src/check-types/is-dynamic-setter.test.ts @@ -0,0 +1,28 @@ +import { Component } from 'react'; +import { isDynamicSetter } from '../../../src/check-types/is-dynamic-setter'; + +describe('isDynamicSetter', () => { + it('returns true if input is a dynamic setter function', () => { + const dynamicSetter = (value: any) => { + // some implementation + }; + + expect(isDynamicSetter(dynamicSetter)).toBeTruthy(); + }); + + it('returns false if input is not a dynamic setter function', () => { + expect(isDynamicSetter('not a function')).toBeFalsy(); + expect(isDynamicSetter(null)).toBeFalsy(); + expect(isDynamicSetter(undefined)).toBeFalsy(); + expect(isDynamicSetter(2)).toBeFalsy(); + expect(isDynamicSetter(0)).toBeFalsy(); + }); + + it('returns false if input is a React class', () => { + class ReactClass extends Component { + // some implementation + } + + expect(isDynamicSetter(ReactClass)).toBeFalsy(); + }); +}); diff --git a/packages/utils/test/src/check-types/is-i18n-data.test.ts b/packages/utils/test/src/check-types/is-i18n-data.test.ts new file mode 100644 index 0000000000..2e903a2ed2 --- /dev/null +++ b/packages/utils/test/src/check-types/is-i18n-data.test.ts @@ -0,0 +1,27 @@ +import { isI18nData } from '../../../src/check-types/is-i18n-data'; +import { IPublicTypeI18nData } from "@alilc/lowcode-types"; + +describe('isI18nData', () => { + it('should return true for valid i18n data', () => { + const i18nData: IPublicTypeI18nData = { + type: 'i18n', + // add any other required properties here + }; + + expect(isI18nData(i18nData)).toBe(true); + }); + + it('should return false for invalid i18n data', () => { + const invalidData = { + type: 'some-other-type', + // add any other properties here + }; + + expect(isI18nData(invalidData)).toBe(false); + }); + + it('should return false for undefined or null', () => { + expect(isI18nData(undefined)).toBe(false); + expect(isI18nData(null)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-isfunction.test.ts b/packages/utils/test/src/check-types/is-isfunction.test.ts new file mode 100644 index 0000000000..5154282285 --- /dev/null +++ b/packages/utils/test/src/check-types/is-isfunction.test.ts @@ -0,0 +1,61 @@ +import { isInnerJsFunction, isJSFunction } from '../../../src/check-types/is-isfunction'; + +describe('isInnerJsFunction', () => { + test('should return true for valid input', () => { + const data = { + type: 'JSExpression', + source: '', + value: '', + extType: 'function' + }; + + expect(isInnerJsFunction(data)).toBe(true); + }); + + test('should return false for invalid input', () => { + const data = { + type: 'JSExpression', + source: '', + value: '', + extType: 'object' + }; + + expect(isInnerJsFunction(data)).toBe(false); + expect(isInnerJsFunction(null)).toBe(false); + expect(isInnerJsFunction(undefined)).toBe(false); + expect(isInnerJsFunction(1)).toBe(false); + expect(isInnerJsFunction(0)).toBe(false); + expect(isInnerJsFunction('string')).toBe(false); + expect(isInnerJsFunction('')).toBe(false); + }); +}); + +describe('isJSFunction', () => { + test('should return true for valid input', () => { + const data = { + type: 'JSFunction', + }; + + expect(isJSFunction(data)).toBe(true); + }); + + test('should return true for inner js function', () => { + const data = { + type: 'JSExpression', + source: '', + value: '', + extType: 'function' + }; + + expect(isJSFunction(data)).toBe(true); + }); + + test('should return false for invalid input', () => { + expect(isJSFunction(null)).toBe(false); + expect(isJSFunction(undefined)).toBe(false); + expect(isJSFunction('string')).toBe(false); + expect(isJSFunction('')).toBe(false); + expect(isJSFunction(0)).toBe(false); + expect(isJSFunction(2)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-jsblock.test.ts b/packages/utils/test/src/check-types/is-jsblock.test.ts new file mode 100644 index 0000000000..e44e9eb705 --- /dev/null +++ b/packages/utils/test/src/check-types/is-jsblock.test.ts @@ -0,0 +1,22 @@ +import { isJSBlock } from '../../../src/check-types/is-jsblock'; + +describe('isJSBlock', () => { + it('should return false if data is null or undefined', () => { + expect(isJSBlock(null)).toBe(false); + expect(isJSBlock(undefined)).toBe(false); + }); + + it('should return false if data is not an object', () => { + expect(isJSBlock('JSBlock')).toBe(false); + expect(isJSBlock(123)).toBe(false); + expect(isJSBlock(true)).toBe(false); + }); + + it('should return false if data.type is not "JSBlock"', () => { + expect(isJSBlock({ type: 'InvalidType' })).toBe(false); + }); + + it('should return true if data is an object and data.type is "JSBlock"', () => { + expect(isJSBlock({ type: 'JSBlock' })).toBe(true); + }); +}); diff --git a/packages/utils/test/src/check-types/is-jsexpression.test.ts b/packages/utils/test/src/check-types/is-jsexpression.test.ts new file mode 100644 index 0000000000..dd8509a3b3 --- /dev/null +++ b/packages/utils/test/src/check-types/is-jsexpression.test.ts @@ -0,0 +1,39 @@ +import { isJSExpression } from '../../../src/check-types/is-jsexpression'; + +describe('isJSExpression', () => { + it('should return true if the input is a valid JSExpression object', () => { + const validJSExpression = { + type: 'JSExpression', + extType: 'variable', + }; + + const result = isJSExpression(validJSExpression); + + expect(result).toBe(true); + }); + + it('should return false if the input is not a valid JSExpression object', () => { + const invalidJSExpression = { + type: 'JSExpression', + extType: 'function', + }; + + const result = isJSExpression(invalidJSExpression); + + expect(result).toBe(false); + }); + + it('should return false if the input is null', () => { + const result = isJSExpression(null); + + expect(result).toBe(false); + }); + + it('should return false if the input is undefined', () => { + const result = isJSExpression(undefined); + + expect(result).toBe(false); + }); + + // 添加其他需要的测试 +}); diff --git a/packages/utils/test/src/check-types/is-jsslot.test.ts b/packages/utils/test/src/check-types/is-jsslot.test.ts new file mode 100644 index 0000000000..5c130cddfd --- /dev/null +++ b/packages/utils/test/src/check-types/is-jsslot.test.ts @@ -0,0 +1,37 @@ +import { isJSSlot } from '../../../src/check-types/is-jsslot'; +import { IPublicTypeJSSlot } from '@alilc/lowcode-types'; + +describe('isJSSlot', () => { + it('should return true when input is of type IPublicTypeJSSlot', () => { + const input: IPublicTypeJSSlot = { + type: 'JSSlot', + // other properties of IPublicTypeJSSlot + }; + + const result = isJSSlot(input); + + expect(result).toBe(true); + }); + + it('should return false when input is not of type IPublicTypeJSSlot', () => { + const input = { + type: 'OtherType', + // other properties + }; + + const result = isJSSlot(input); + + expect(result).toBe(false); + }); + + it('should return false when input is null or undefined', () => { + const input1 = null; + const input2 = undefined; + + const result1 = isJSSlot(input1); + const result2 = isJSSlot(input2); + + expect(result1).toBe(false); + expect(result2).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-location-children-detail.test.ts b/packages/utils/test/src/check-types/is-location-children-detail.test.ts new file mode 100644 index 0000000000..f209e8e63f --- /dev/null +++ b/packages/utils/test/src/check-types/is-location-children-detail.test.ts @@ -0,0 +1,27 @@ +import { isLocationChildrenDetail } from '../../../src/check-types/is-location-children-detail'; +import { IPublicTypeLocationChildrenDetail, IPublicTypeLocationDetailType } from '@alilc/lowcode-types'; + +describe('isLocationChildrenDetail', () => { + it('should return true when obj is IPublicTypeLocationChildrenDetail', () => { + const obj: IPublicTypeLocationChildrenDetail = { + type: IPublicTypeLocationDetailType.Children, + // 添加其他必要的属性 + }; + + expect(isLocationChildrenDetail(obj)).toBe(true); + }); + + it('should return false when obj is not IPublicTypeLocationChildrenDetail', () => { + const obj = { + type: 'other', + // 添加其他必要的属性 + }; + + expect(isLocationChildrenDetail(obj)).toBe(false); + expect(isLocationChildrenDetail(null)).toBe(false); + expect(isLocationChildrenDetail(undefined)).toBe(false); + expect(isLocationChildrenDetail('string')).toBe(false); + expect(isLocationChildrenDetail(0)).toBe(false); + expect(isLocationChildrenDetail(2)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-location-data.test.ts b/packages/utils/test/src/check-types/is-location-data.test.ts new file mode 100644 index 0000000000..ba2e2c8be0 --- /dev/null +++ b/packages/utils/test/src/check-types/is-location-data.test.ts @@ -0,0 +1,44 @@ +import { isLocationData } from '../../../src/check-types/is-location-data'; +import { IPublicTypeLocationData } from '@alilc/lowcode-types'; + +describe('isLocationData', () => { + it('should return true when obj is valid location data', () => { + const obj: IPublicTypeLocationData = { + target: 'some target', + detail: 'some detail', + }; + + const result = isLocationData(obj); + + expect(result).toBe(true); + }); + + it('should return false when obj is missing target or detail', () => { + const obj1 = { + target: 'some target', + // missing detail + }; + + const obj2 = { + // missing target + detail: 'some detail', + }; + + const result1 = isLocationData(obj1); + const result2 = isLocationData(obj2); + + expect(result1).toBe(false); + expect(result2).toBe(false); + }); + + it('should return false when obj is null or undefined', () => { + const obj1 = null; + const obj2 = undefined; + + const result1 = isLocationData(obj1); + const result2 = isLocationData(obj2); + + expect(result1).toBe(false); + expect(result2).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-lowcode-component-type.test.ts b/packages/utils/test/src/check-types/is-lowcode-component-type.test.ts new file mode 100644 index 0000000000..35b76f00b5 --- /dev/null +++ b/packages/utils/test/src/check-types/is-lowcode-component-type.test.ts @@ -0,0 +1,21 @@ +import { isLowCodeComponentType } from '../../../src/check-types/is-lowcode-component-type'; +import { IPublicTypeLowCodeComponent, IPublicTypeProCodeComponent } from '@alilc/lowcode-types'; + +describe('isLowCodeComponentType', () => { + test('should return true for a low code component type', () => { + const desc: IPublicTypeLowCodeComponent = { + // create a valid low code component description + }; + + expect(isLowCodeComponentType(desc)).toBe(true); + }); + + test('should return false for a pro code component type', () => { + const desc: IPublicTypeProCodeComponent = { + // create a valid pro code component description + package: 'pro-code' + }; + + expect(isLowCodeComponentType(desc)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-lowcode-project-schema.test.ts b/packages/utils/test/src/check-types/is-lowcode-project-schema.test.ts new file mode 100644 index 0000000000..bb750ed88b --- /dev/null +++ b/packages/utils/test/src/check-types/is-lowcode-project-schema.test.ts @@ -0,0 +1,42 @@ +import { isLowcodeProjectSchema } from "../../../src/check-types/is-lowcode-project-schema"; + +describe("isLowcodeProjectSchema", () => { + it("should return false when data is null", () => { + const result = isLowcodeProjectSchema(null); + expect(result).toBe(false); + }); + + it("should return false when data is undefined", () => { + const result = isLowcodeProjectSchema(undefined); + expect(result).toBe(false); + }); + + it("should return false when data is not an object", () => { + const result = isLowcodeProjectSchema("not an object"); + expect(result).toBe(false); + }); + + it("should return false when componentsTree is missing", () => { + const data = { someKey: "someValue" }; + const result = isLowcodeProjectSchema(data); + expect(result).toBe(false); + }); + + it("should return false when componentsTree is an empty array", () => { + const data = { componentsTree: [] }; + const result = isLowcodeProjectSchema(data); + expect(result).toBe(false); + }); + + it("should return false when the first element of componentsTree is not a component schema", () => { + const data = { componentsTree: [{}] }; + const result = isLowcodeProjectSchema(data); + expect(result).toBe(false); + }); + + it("should return true when all conditions are met", () => { + const data = { componentsTree: [{ prop: "value", componentName: 'Component' }] }; + const result = isLowcodeProjectSchema(data); + expect(result).toBe(true); + }); +}); diff --git a/packages/utils/test/src/check-types/is-node-schema.test.ts b/packages/utils/test/src/check-types/is-node-schema.test.ts new file mode 100644 index 0000000000..b5a4e39acb --- /dev/null +++ b/packages/utils/test/src/check-types/is-node-schema.test.ts @@ -0,0 +1,43 @@ +import { isNodeSchema } from '../../../src/check-types/is-node-schema'; + +describe('isNodeSchema', () => { + // 测试正常情况 + it('should return true for valid IPublicTypeNodeSchema', () => { + const validData = { + componentName: 'Component', + isNode: false, + }; + expect(isNodeSchema(validData)).toBe(true); + }); + + // 测试 null 或 undefined + it('should return false for null or undefined', () => { + expect(isNodeSchema(null)).toBe(false); + expect(isNodeSchema(undefined)).toBe(false); + }); + + // 测试没有componentName属性的情况 + it('should return false if componentName is missing', () => { + const invalidData = { + isNode: false, + }; + expect(isNodeSchema(invalidData)).toBe(false); + }); + + // 测试isNode为true的情况 + it('should return false if isNode is true', () => { + const invalidData = { + componentName: 'Component', + isNode: true, + }; + expect(isNodeSchema(invalidData)).toBe(false); + }); + + // 测试其他数据类型的情况 + it('should return false for other data types', () => { + expect(isNodeSchema('string')).toBe(false); + expect(isNodeSchema(123)).toBe(false); + expect(isNodeSchema([])).toBe(false); + expect(isNodeSchema({})).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-node.test.ts b/packages/utils/test/src/check-types/is-node.test.ts new file mode 100644 index 0000000000..d6d8dfc03d --- /dev/null +++ b/packages/utils/test/src/check-types/is-node.test.ts @@ -0,0 +1,19 @@ +import { isNode } from '../../../src/check-types/is-node'; + +describe('isNode', () => { + it('should return true for a valid node', () => { + const node = { isNode: true }; + expect(isNode(node)).toBeTruthy(); + }); + + it('should return false for an invalid node', () => { + const node = { isNode: false }; + expect(isNode(node)).toBeFalsy(); + }); + + it('should return false for an undefined node', () => { + expect(isNode(undefined)).toBeFalsy(); + }); + + // Add more test cases if needed +}); diff --git a/packages/utils/test/src/check-types/is-procode-component-type.test.ts b/packages/utils/test/src/check-types/is-procode-component-type.test.ts new file mode 100644 index 0000000000..58f435b98a --- /dev/null +++ b/packages/utils/test/src/check-types/is-procode-component-type.test.ts @@ -0,0 +1,13 @@ +import { isProCodeComponentType } from '../../../src/check-types/is-procode-component-type'; + +describe('isProCodeComponentType', () => { + it('should return true if the given desc object contains "package" property', () => { + const desc = { package: 'packageName' }; + expect(isProCodeComponentType(desc)).toBe(true); + }); + + it('should return false if the given desc object does not contain "package" property', () => { + const desc = { name: 'componentName' }; + expect(isProCodeComponentType(desc)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-project-schema.test.ts b/packages/utils/test/src/check-types/is-project-schema.test.ts new file mode 100644 index 0000000000..0ec3f47408 --- /dev/null +++ b/packages/utils/test/src/check-types/is-project-schema.test.ts @@ -0,0 +1,28 @@ +import { IPublicTypeProjectSchema } from "@alilc/lowcode-types"; +import { isProjectSchema } from "../../../src/check-types/is-project-schema"; + +describe("isProjectSchema", () => { + it("should return true if data has componentsTree property", () => { + const data: IPublicTypeProjectSchema = { + // ... + componentsTree: { + // ... + }, + }; + expect(isProjectSchema(data)).toBe(true); + }); + + it("should return false if data does not have componentsTree property", () => { + const data = { + // ... + }; + expect(isProjectSchema(data)).toBe(false); + }); + + it("should return false if data is null or undefined", () => { + expect(isProjectSchema(null)).toBe(false); + expect(isProjectSchema(undefined)).toBe(false); + }); + + // 更多的测试用例... +}); 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/utils/test/src/check-types/is-setter-config.test.ts b/packages/utils/test/src/check-types/is-setter-config.test.ts new file mode 100644 index 0000000000..eee234658d --- /dev/null +++ b/packages/utils/test/src/check-types/is-setter-config.test.ts @@ -0,0 +1,26 @@ +import { isSetterConfig } from '../../../src/check-types/is-setter-config'; + +describe('isSetterConfig', () => { + test('should return true for valid setter config', () => { + const config = { + componentName: 'MyComponent', + // Add other required properties here + }; + + expect(isSetterConfig(config)).toBe(true); + }); + + test('should return false for invalid setter config', () => { + const config = { + // Missing componentName property + }; + + expect(isSetterConfig(config)).toBe(false); + expect(isSetterConfig(null)).toBe(false); + expect(isSetterConfig(undefined)).toBe(false); + expect(isSetterConfig(0)).toBe(false); + expect(isSetterConfig(2)).toBe(false); + }); + + // Add more test cases for different scenarios you want to cover +}); diff --git a/packages/utils/test/src/check-types/is-setting-field.test.ts b/packages/utils/test/src/check-types/is-setting-field.test.ts new file mode 100644 index 0000000000..5f9bbd6239 --- /dev/null +++ b/packages/utils/test/src/check-types/is-setting-field.test.ts @@ -0,0 +1,18 @@ +import { isSettingField } from "../../../src/check-types/is-setting-field"; + +describe("isSettingField", () => { + it("should return true for an object that has isSettingField property", () => { + const obj = { isSettingField: true }; + expect(isSettingField(obj)).toBe(true); + }); + + it("should return false for an object that does not have isSettingField property", () => { + const obj = { foo: "bar" }; + expect(isSettingField(obj)).toBe(false); + }); + + it("should return false for a falsy value", () => { + const obj = null; + expect(isSettingField(obj)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/check-types/is-title-config.test.ts b/packages/utils/test/src/check-types/is-title-config.test.ts new file mode 100644 index 0000000000..4aa6d219cb --- /dev/null +++ b/packages/utils/test/src/check-types/is-title-config.test.ts @@ -0,0 +1,18 @@ +import { isTitleConfig } from '../../../src/check-types/is-title-config'; + +describe('isTitleConfig', () => { + it('should return true for valid config object', () => { + const config = { title: 'My Title' }; + expect(isTitleConfig(config)).toBe(true); + }); + + it('should return false for invalid config object', () => { + const config = { title: 'My Title', type: 'i18n' , i18nData: {} }; + expect(isTitleConfig(config)).toBe(false); + }); + + it('should return false for non-object input', () => { + const config = 'invalid'; + expect(isTitleConfig(config)).toBe(false); + }); +}); diff --git a/packages/utils/test/src/clone-deep.test.ts b/packages/utils/test/src/clone-deep.test.ts new file mode 100644 index 0000000000..58fabc6f68 --- /dev/null +++ b/packages/utils/test/src/clone-deep.test.ts @@ -0,0 +1,30 @@ +import { cloneDeep } from '../../src/clone-deep'; + +describe('cloneDeep', () => { + it('should clone null', () => { + const src = null; + expect(cloneDeep(src)).toBeNull(); + }); + + it('should clone undefined', () => { + const src = undefined; + expect(cloneDeep(src)).toBeUndefined(); + }); + + it('should clone an array', () => { + const src = [1, 2, 3, 4]; + expect(cloneDeep(src)).toEqual(src); + }); + + it('should clone an object', () => { + const src = { name: 'John', age: 25 }; + expect(cloneDeep(src)).toEqual(src); + }); + + it('should deep clone nested objects', () => { + const src = { person: { name: 'John', age: 25 } }; + const cloned = cloneDeep(src); + expect(cloned).toEqual(src); + expect(cloned.person).not.toBe(src.person); + }); +}); \ No newline at end of file diff --git a/packages/utils/test/src/clone-enumerable-property.test.ts b/packages/utils/test/src/clone-enumerable-property.test.ts new file mode 100644 index 0000000000..2eff09e44c --- /dev/null +++ b/packages/utils/test/src/clone-enumerable-property.test.ts @@ -0,0 +1,30 @@ +import { cloneEnumerableProperty } from '../../src/clone-enumerable-property'; + +describe('cloneEnumerableProperty', () => { + test('should clone enumerable properties from origin to target', () => { + // Arrange + const target = {}; + const origin = { prop1: 1, prop2: 'hello', prop3: true }; + + // Act + const result = cloneEnumerableProperty(target, origin); + + // Assert + expect(result).toBe(target); + expect(result).toEqual(origin); + }); + + test('should exclude properties specified in excludePropertyNames', () => { + // Arrange + const target = {}; + const origin = { prop1: 1, prop2: 'hello', prop3: true }; + const excludePropertyNames = ['prop2']; + + // Act + const result = cloneEnumerableProperty(target, origin, excludePropertyNames); + + // Assert + expect(result).toBe(target); + expect(result).toEqual({ prop1: 1, prop3: true }); + }); +}); \ No newline at end of file diff --git a/packages/utils/test/src/create-content.test.tsx b/packages/utils/test/src/create-content.test.tsx new file mode 100644 index 0000000000..c41fb0f0da --- /dev/null +++ b/packages/utils/test/src/create-content.test.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { createContent } from '../../src/create-content'; + +const MyComponent = () => { + return <div>MyComponent</div> +} +describe('createContent', () => { + test('should return the same content if it is a valid React element', () => { + const content = <div>Hello</div>; + const result = createContent(content); + + expect(result).toEqual(content); + }); + + test('should clone the element with props if props are provided', () => { + const content = <div></div>; + const props = { className: 'my-class' }; + const result = createContent(content, props); + + expect(result.props).toEqual(props); + }); + + test('should create an element with props if the content is a React component', () => { + const content = MyComponent; + const props = { className: 'my-class' }; + const result = createContent(content, props); + + expect(result.type).toEqual(content); + expect(result.props).toEqual(props); + }); + + test('should return the content if it is not a React element or a React component', () => { + const content = 'Hello'; + const result = createContent(content); + + expect(result).toEqual(content); + }); +}); diff --git a/packages/utils/test/src/create-defer.test.ts b/packages/utils/test/src/create-defer.test.ts new file mode 100644 index 0000000000..c6ab9207a9 --- /dev/null +++ b/packages/utils/test/src/create-defer.test.ts @@ -0,0 +1,16 @@ +import { createDefer } from '../../src/create-defer'; + +describe('createDefer', () => { + it('should resolve with given value', async () => { + const defer = createDefer<number>(); + defer.resolve(42); + const result = await defer.promise(); + expect(result).toBe(42); + }); + + it('should reject with given reason', async () => { + const defer = createDefer<number>(); + defer.reject('error'); + await expect(defer.promise()).rejects.toEqual('error'); + }); +}); diff --git a/packages/utils/test/src/is-object.test.ts b/packages/utils/test/src/is-object.test.ts new file mode 100644 index 0000000000..7ae984b8f8 --- /dev/null +++ b/packages/utils/test/src/is-object.test.ts @@ -0,0 +1,45 @@ +import { isObject, isI18NObject } from '../../src/is-object'; + +describe('isObject', () => { + it('should return true for an object', () => { + const obj = { key: 'value' }; + const result = isObject(obj); + expect(result).toBe(true); + }); + + it('should return false for null', () => { + const result = isObject(null); + expect(result).toBe(false); + }); + + it('should return false for a non-object value', () => { + const value = 42; // Not an object + const result = isObject(value); + expect(result).toBe(false); + }); +}); + +describe('isI18NObject', () => { + it('should return true for an I18N object', () => { + const i18nObject = { type: 'i18n', data: 'some data' }; + const result = isI18NObject(i18nObject); + expect(result).toBe(true); + }); + + it('should return false for a non-I18N object', () => { + const nonI18nObject = { type: 'other', data: 'some data' }; + const result = isI18NObject(nonI18nObject); + expect(result).toBe(false); + }); + + it('should return false for null', () => { + const result = isI18NObject(null); + expect(result).toBe(false); + }); + + it('should return false for a non-object value', () => { + const value = 42; // Not an object + const result = isI18NObject(value); + expect(result).toBe(false); + }); +}); diff --git a/packages/utils/test/src/is-react.test.tsx b/packages/utils/test/src/is-react.test.tsx new file mode 100644 index 0000000000..9ed2bd6c38 --- /dev/null +++ b/packages/utils/test/src/is-react.test.tsx @@ -0,0 +1,316 @@ +import React, { Component, createElement } from "react"; +import { + isReactComponent, + wrapReactClass, + isForwardOrMemoForward, + isMemoType, + isForwardRefType, + acceptsRef, + isReactClass, + REACT_MEMO_TYPE, + REACT_FORWARD_REF_TYPE, + } from "../../src/is-react"; + +class reactDemo extends React.Component { + +} + +const reactMemo = React.memo(reactDemo); + +const reactForwardRef = React.forwardRef((props, ref): any => { + return ''; +}); + +describe('is-react-ut', () => { + it('isReactComponent', () => { + expect(isReactComponent(null)).toBeFalsy(); + expect(isReactComponent(() => {})).toBeTruthy(); + expect(isReactComponent({ + $$typeof: Symbol.for('react.memo') + })).toBeTruthy(); + expect(isReactComponent({ + $$typeof: Symbol.for('react.forward_ref') + })).toBeTruthy(); + expect(isReactComponent(reactDemo)).toBeTruthy(); + expect(isReactComponent(reactMemo)).toBeTruthy(); + expect(isReactComponent(reactForwardRef)).toBeTruthy(); + + }); + + it('wrapReactClass', () => { + const wrap = wrapReactClass(() => {}); + expect(isReactComponent(wrap)).toBeTruthy(); + + const fun = () => {}; + fun.displayName = 'mock'; + expect(wrapReactClass(fun).displayName).toBe('mock'); + }) +}) + +describe('wrapReactClass', () => { + it('should wrap a FunctionComponent', () => { + // Create a mock FunctionComponent + const MockComponent: React.FunctionComponent = (props) => { + return <div>{props.children}</div>; + }; + + // Wrap the FunctionComponent using wrapReactClass + const WrappedComponent = wrapReactClass(MockComponent); + const instance = new WrappedComponent(); + + // Check if the WrappedComponent extends Component + expect(instance instanceof React.Component).toBe(true); + }); + + it('should render the FunctionComponent with props', () => { + // Create a mock FunctionComponent + const MockComponent: React.FunctionComponent = (props) => { + return <div>{props.children}</div>; + }; + + MockComponent.displayName = 'FunctionComponent'; + + // Wrap the FunctionComponent using wrapReactClass + const WrappedComponent = wrapReactClass(MockComponent); + + // Create some test props + const testProps = { prop1: 'value1', prop2: 'value2' }; + + // Render the WrappedComponent with test props + const rendered = createElement(WrappedComponent, testProps, 'Child Text'); + + // Check if the WrappedComponent renders the FunctionComponent with props + expect(rendered).toMatchSnapshot(); + }); +}); + +describe('isReactComponent', () => { + it('should identify a class component as a React component', () => { + class ClassComponent extends React.Component { + render() { + return <div>Class Component</div>; + } + } + + expect(isReactComponent(ClassComponent)).toBe(true); + }); + + it('should identify a functional component as a React component', () => { + const FunctionalComponent = () => { + return <div>Functional Component</div>; + }; + + expect(isReactComponent(FunctionalComponent)).toBe(true); + }); + + it('should identify a forward ref component as a React component', () => { + const ForwardRefComponent = React.forwardRef((props, ref) => { + return <div ref={ref}>Forward Ref Component</div>; + }); + + expect(isReactComponent(ForwardRefComponent)).toBe(true); + }); + + it('should identify a memo component as a React component', () => { + const MemoComponent = React.memo(() => { + return <div>Memo Component</div>; + }); + + expect(isReactComponent(MemoComponent)).toBe(true); + }); + + it('should return false for non-React components', () => { + const plainObject = { prop: 'value' }; + const notAComponent = 'Not a component'; + + expect(isReactComponent(plainObject)).toBe(false); + expect(isReactComponent(notAComponent)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(isReactComponent(nullValue)).toBe(false); + expect(isReactComponent(undefinedValue)).toBe(false); + }); +}); + +describe('isForwardOrMemoForward', () => { + it('should return true for a forwardRef component', () => { + const forwardRefComponent = React.forwardRef(() => { + return <div>ForwardRef Component</div>; + }); + + expect(isForwardOrMemoForward(forwardRefComponent)).toBe(true); + }); + + it('should return true for a memoized forwardRef component', () => { + const forwardRefComponent = React.forwardRef(() => { + return <div>ForwardRef Component</div>; + }); + + const memoizedComponent = React.memo(forwardRefComponent); + + expect(isForwardOrMemoForward(memoizedComponent)).toBe(true); + }); + + it('should return false for a memoized component that is not a forwardRef', () => { + const memoizedComponent = React.memo(() => { + return <div>Memoized Component</div>; + }); + + expect(isForwardOrMemoForward(memoizedComponent)).toBe(false); + }); + + it('should return false for a plain object', () => { + const plainObject = { prop: 'value' }; + + expect(isForwardOrMemoForward(plainObject)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(isForwardOrMemoForward(nullValue)).toBe(false); + expect(isForwardOrMemoForward(undefinedValue)).toBe(false); + }); +}); + +describe('isMemoType', () => { + it('should return true for an object with $$typeof matching REACT_MEMO_TYPE', () => { + const memoTypeObject = { $$typeof: REACT_MEMO_TYPE }; + + expect(isMemoType(memoTypeObject)).toBe(true); + }); + + it('should return false for an object with $$typeof not matching REACT_MEMO_TYPE', () => { + const otherTypeObject = { $$typeof: Symbol.for('other.type') }; + + expect(isMemoType(otherTypeObject)).toBe(false); + }); + + it('should return false for an object with no $$typeof property', () => { + const noTypeObject = { key: 'value' }; + + expect(isMemoType(noTypeObject)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(isMemoType(nullValue)).toBe(false); + expect(isMemoType(undefinedValue)).toBe(false); + }); +}); + +describe('isForwardRefType', () => { + it('should return true for an object with $$typeof matching REACT_FORWARD_REF_TYPE', () => { + const forwardRefTypeObject = { $$typeof: REACT_FORWARD_REF_TYPE }; + + expect(isForwardRefType(forwardRefTypeObject)).toBe(true); + }); + + it('should return false for an object with $$typeof not matching REACT_FORWARD_REF_TYPE', () => { + const otherTypeObject = { $$typeof: Symbol.for('other.type') }; + + expect(isForwardRefType(otherTypeObject)).toBe(false); + }); + + it('should return false for an object with no $$typeof property', () => { + const noTypeObject = { key: 'value' }; + + expect(isForwardRefType(noTypeObject)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(isForwardRefType(nullValue)).toBe(false); + expect(isForwardRefType(undefinedValue)).toBe(false); + }); +}); + +describe('acceptsRef', () => { + it('should return true for an object with isReactComponent in its prototype', () => { + const objWithIsReactComponent = { + prototype: { + isReactComponent: true, + }, + }; + + expect(acceptsRef(objWithIsReactComponent)).toBe(true); + }); + + it('should return true for an object that is forwardRef or memoized forwardRef', () => { + const forwardRefObject = React.forwardRef(() => { + return null; + }); + + const memoizedForwardRefObject = React.memo(forwardRefObject); + + expect(acceptsRef(forwardRefObject)).toBe(true); + expect(acceptsRef(memoizedForwardRefObject)).toBe(true); + }); + + it('should return false for an object without isReactComponent in its prototype', () => { + const objWithoutIsReactComponent = { + prototype: { + someOtherProperty: true, + }, + }; + + expect(acceptsRef(objWithoutIsReactComponent)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(acceptsRef(nullValue)).toBe(false); + expect(acceptsRef(undefinedValue)).toBe(false); + }); +}); + +describe('isReactClass', () => { + it('should return true for an object with isReactComponent in its prototype', () => { + class ReactClassComponent extends Component { + render() { + return null; + } + } + + expect(isReactClass(ReactClassComponent)).toBe(true); + }); + + it('should return true for an object with Component in its prototype chain', () => { + class CustomComponent extends Component { + render() { + return null; + } + } + + expect(isReactClass(CustomComponent)).toBe(true); + }); + + it('should return false for an object without isReactComponent in its prototype', () => { + class NonReactComponent { + render() { + return null; + } + } + + expect(isReactClass(NonReactComponent)).toBe(false); + }); + + it('should return false for null or undefined', () => { + const nullValue = null; + const undefinedValue = undefined; + + expect(isReactClass(nullValue)).toBe(false); + expect(isReactClass(undefinedValue)).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/utils/test/src/is-shaken.test.ts b/packages/utils/test/src/is-shaken.test.ts new file mode 100644 index 0000000000..35a27af5f6 --- /dev/null +++ b/packages/utils/test/src/is-shaken.test.ts @@ -0,0 +1,45 @@ +import { isShaken } from '../../src/is-shaken'; + +describe('isShaken', () => { + it('should return true if e1 has shaken property', () => { + const e1: any = { shaken: true }; + const e2: MouseEvent | DragEvent = { target: null } as MouseEvent | DragEvent; + + expect(isShaken(e1, e2)).toBe(true); + }); + + it('should return true if e1.target and e2.target are different', () => { + const e1: MouseEvent | DragEvent = { target: {} } as MouseEvent | DragEvent; + const e2: MouseEvent | DragEvent = { target: {} } as MouseEvent | DragEvent; + + expect(isShaken(e1, e2)).toBe(true); + }); + + it('should return false if e1 and e2 targets are the same and distance is less than SHAKE_DISTANCE', () => { + const target = {}; + const e1: MouseEvent | DragEvent = { target: target } as MouseEvent | DragEvent; + const e2: MouseEvent | DragEvent = { target: target } as MouseEvent | DragEvent; + + // Assuming SHAKE_DISTANCE is 100 + e1.clientY = 50; + e2.clientY = 50; + + e1.clientX = 60; + e2.clientX = 60; + + expect(isShaken(e1, e2)).toBe(false); + }); + + it('should return true if e1 and e2 targets are the same and distance is greater than SHAKE_DISTANCE', () => { + const e1: MouseEvent | DragEvent = { target: {} } as MouseEvent | DragEvent; + const e2: MouseEvent | DragEvent = { target: {} } as MouseEvent | DragEvent; + + // Assuming SHAKE_DISTANCE is 100 + e1.clientY = 50; + e1.clientX = 50; + e2.clientY = 200; + e2.clientX = 200; + + expect(isShaken(e1, e2)).toBe(true); + }); +}); diff --git a/packages/utils/test/src/misc.test.ts b/packages/utils/test/src/misc.test.ts new file mode 100644 index 0000000000..2514661508 --- /dev/null +++ b/packages/utils/test/src/misc.test.ts @@ -0,0 +1,326 @@ +import { + isVariable, + isUseI18NSetter, + convertToI18NObject, + isString, + waitForThing, + arrShallowEquals, + isFromVC, + executePendingFn, + compatStage, + invariant, + isRegExp, + shouldUseVariableSetter, +} from '../../src/misc'; +import { IPublicModelComponentMeta } from '@alilc/lowcode-types'; + +describe('isVariable', () => { + it('should return true for a variable object', () => { + const variable = { type: 'variable', variable: 'foo', value: 'bar' }; + const result = isVariable(variable); + expect(result).toBe(true); + }); + + it('should return false for non-variable objects', () => { + const obj = { type: 'object' }; + const result = isVariable(obj); + expect(result).toBe(false); + }); +}); + +describe('isUseI18NSetter', () => { + it('should return true for a property with I18nSetter', () => { + const prototype = { options: { configure: [{ name: 'propName', setter: { type: { displayName: 'I18nSetter' } } }] } }; + const propName = 'propName'; + const result = isUseI18NSetter(prototype, propName); + expect(result).toBe(true); + }); + + it('should return false for a property without I18nSetter', () => { + const prototype = { options: { configure: [{ name: 'propName', setter: { type: { displayName: 'OtherSetter' } } }] } }; + const propName = 'propName'; + const result = isUseI18NSetter(prototype, propName); + expect(result).toBe(false); + }); +}); + +describe('convertToI18NObject', () => { + it('should return the input if it is already an I18N object', () => { + const i18nObject = { type: 'i18n', use: 'en', en: 'Hello' }; + const result = convertToI18NObject(i18nObject); + expect(result).toEqual(i18nObject); + }); + + it('should convert a string to an I18N object', () => { + const inputString = 'Hello'; + const result = convertToI18NObject(inputString); + const expectedOutput = { type: 'i18n', use: 'zh-CN', 'zh-CN': inputString }; + expect(result).toEqual(expectedOutput); + }); +}); + +describe('isString', () => { + it('should return true for a string', () => { + const stringValue = 'Hello, world!'; + const result = isString(stringValue); + expect(result).toBe(true); + }); + + it('should return true for an empty string', () => { + const emptyString = ''; + const result = isString(emptyString); + expect(result).toBe(true); + }); + + it('should return false for a number', () => { + const numberValue = 42; // Not a string + const result = isString(numberValue); + expect(result).toBe(false); + }); + + it('should return false for an object', () => { + const objectValue = { key: 'value' }; // Not a string + const result = isString(objectValue); + expect(result).toBe(false); + }); + + it('should return false for null', () => { + const result = isString(null); + expect(result).toBe(false); + }); + + it('should return false for undefined', () => { + const undefinedValue = undefined; + const result = isString(undefinedValue); + expect(result).toBe(false); + }); + + it('should return false for a boolean', () => { + const booleanValue = true; // Not a string + const result = isString(booleanValue); + expect(result).toBe(false); + }); +}); + +describe('waitForThing', () => { + it('should resolve immediately if the thing is available', async () => { + const obj = { prop: 'value' }; + const path = 'prop'; + const result = await waitForThing(obj, path); + expect(result).toBe('value'); + }); + + it('should resolve after a delay if the thing becomes available', async () => { + const obj = { prop: undefined }; + const path = 'prop'; + const delay = 100; // Adjust the delay as needed + setTimeout(() => { + obj.prop = 'value'; + }, delay); + + const result = await waitForThing(obj, path); + expect(result).toBe('value'); + }); +}); + +describe('arrShallowEquals', () => { + it('should return true for two empty arrays', () => { + const arr1 = []; + const arr2 = []; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(true); + }); + + it('should return true for two arrays with the same elements in the same order', () => { + const arr1 = [1, 2, 3]; + const arr2 = [1, 2, 3]; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(true); + }); + + it('should return true for two arrays with the same elements in a different order', () => { + const arr1 = [1, 2, 3]; + const arr2 = [3, 2, 1]; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(true); + }); + + it('should return false for two arrays with different lengths', () => { + const arr1 = [1, 2, 3]; + const arr2 = [1, 2]; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(false); + }); + + it('should return false for one array and a non-array', () => { + const arr1 = [1, 2, 3]; + const nonArray = 'not an array'; + const result = arrShallowEquals(arr1, nonArray); + expect(result).toBe(false); + }); + + it('should return false for two arrays with different elements', () => { + const arr1 = [1, 2, 3]; + const arr2 = [3, 4, 5]; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(false); + }); + + it('should return true for arrays with duplicate elements', () => { + const arr1 = [1, 2, 2, 3]; + const arr2 = [2, 3, 3, 1]; + const result = arrShallowEquals(arr1, arr2); + expect(result).toBe(true); + }); +}); + +describe('isFromVC', () => { + it('should return true when advanced configuration is present', () => { + // Create a mock meta object with advanced configuration + const meta: IPublicModelComponentMeta = { + getMetadata: () => ({ configure: { advanced: true } }), + }; + + const result = isFromVC(meta); + + expect(result).toBe(true); + }); + + it('should return false when advanced configuration is not present', () => { + // Create a mock meta object without advanced configuration + const meta: IPublicModelComponentMeta = { + getMetadata: () => ({ configure: { advanced: false } }), + }; + + const result = isFromVC(meta); + + expect(result).toBe(false); + }); + + it('should return false when meta is undefined', () => { + const meta: IPublicModelComponentMeta | undefined = undefined; + + const result = isFromVC(meta); + + expect(result).toBe(false); + }); + + it('should return false when meta does not have configure information', () => { + // Create a mock meta object without configure information + const meta: IPublicModelComponentMeta = { + getMetadata: () => ({}), + }; + + const result = isFromVC(meta); + + expect(result).toBe(false); + }); + + it('should return false when configure.advanced is not present', () => { + // Create a mock meta object with incomplete configure information + const meta: IPublicModelComponentMeta = { + getMetadata: () => ({ configure: {} }), + }; + + const result = isFromVC(meta); + + expect(result).toBe(false); + }); +}); + +describe('executePendingFn', () => { + it('should execute the provided function after the specified timeout', async () => { + // Mock the function to execute + const fn = jest.fn(); + + // Call executePendingFn with the mocked function and a short timeout + executePendingFn(fn, 100); + + // Ensure the function has not been called immediately + expect(fn).not.toHaveBeenCalled(); + + // Wait for the specified timeout + await new Promise(resolve => setTimeout(resolve, 100)); + + // Ensure the function has been called after the timeout + expect(fn).toHaveBeenCalled(); + }); + + it('should execute the provided function with a default timeout if not specified', async () => { + // Mock the function to execute + const fn = jest.fn(); + + // Call executePendingFn with the mocked function without specifying a timeout + executePendingFn(fn); + + // Ensure the function has not been called immediately + expect(fn).not.toHaveBeenCalled(); + + // Wait for the default timeout (2000 milliseconds) + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Ensure the function has been called after the default timeout + expect(fn).toHaveBeenCalled(); + }); +}); + +describe('compatStage', () => { + it('should convert a number to an enum stage', () => { + const result = compatStage(3); + expect(result).toBe('save'); + }); + + it('should warn about the deprecated usage', () => { + const warnSpy = jest.spyOn(console, 'warn'); + const result = compatStage(2); + expect(result).toBe('serilize'); + expect(warnSpy).toHaveBeenCalledWith( + 'stage 直接指定为数字的使用方式已经过时,将在下一版本移除,请直接使用 IPublicEnumTransformStage.Render|Serilize|Save|Clone|Init|Upgrade' + ); + warnSpy.mockRestore(); + }); + + it('should return the enum stage if it is already an enum', () => { + const result = compatStage('render'); + expect(result).toBe('render'); + }); +}); + +describe('invariant', () => { + it('should not throw an error if the check is true', () => { + expect(() => invariant(true, 'Test invariant', 'thing')).not.toThrow(); + }); + + it('should throw an error if the check is false', () => { + expect(() => invariant(false, 'Test invariant', 'thing')).toThrowError( + "Invariant failed: Test invariant in 'thing'" + ); + }); +}); + +describe('isRegExp', () => { + it('should return true for a valid RegExp', () => { + const regex = /test/; + const result = isRegExp(regex); + expect(result).toBe(true); + }); + + it('should return false for a non-RegExp object', () => { + const nonRegExp = { test: /test/ }; + const result = isRegExp(nonRegExp); + expect(result).toBe(false); + }); + + it('should return false for null', () => { + const result = isRegExp(null); + expect(result).toBe(false); + }); +}); + +it('shouldUseVariableSetter', () => { + expect(shouldUseVariableSetter(false, true)).toBeFalsy(); + expect(shouldUseVariableSetter(true, true)).toBeTruthy(); + expect(shouldUseVariableSetter(true, false)).toBeTruthy(); + expect(shouldUseVariableSetter(undefined, false)).toBeFalsy(); + expect(shouldUseVariableSetter(undefined, true)).toBeTruthy(); +}); \ No newline at end of file diff --git a/packages/utils/test/src/navtive-selection.test.ts b/packages/utils/test/src/navtive-selection.test.ts new file mode 100644 index 0000000000..f45d0e1b2a --- /dev/null +++ b/packages/utils/test/src/navtive-selection.test.ts @@ -0,0 +1,18 @@ +import { setNativeSelection, nativeSelectionEnabled } from '../../src/navtive-selection'; + +describe('setNativeSelection', () => { + beforeEach(() => { + // 在每个测试运行之前重置nativeSelectionEnabled的值 + setNativeSelection(true); + }); + + test('should enable native selection', () => { + setNativeSelection(true); + expect(nativeSelectionEnabled).toBe(true); + }); + + test('should disable native selection', () => { + setNativeSelection(false); + expect(nativeSelectionEnabled).toBe(false); + }); +}); diff --git a/packages/utils/test/src/schema.test.ts b/packages/utils/test/src/schema.test.ts index 138dd7a82e..8d03f58118 100644 --- a/packages/utils/test/src/schema.test.ts +++ b/packages/utils/test/src/schema.test.ts @@ -1,4 +1,132 @@ -import { compatibleLegaoSchema } from '../../src/schema'; +import { + compatibleLegaoSchema, + getNodeSchemaById, + applyActivities, +} from '../../src/schema'; +import { ActivityType } from '@alilc/lowcode-types'; + +describe('compatibleLegaoSchema', () => { + it('should handle null input', () => { + const result = compatibleLegaoSchema(null); + expect(result).toBeNull(); + }); + + it('should convert Legao schema to JSExpression', () => { + // Create your test input + const legaoSchema = { + type: 'LegaoType', + source: 'LegaoSource', + compiled: 'LegaoCompiled', + }; + const result = compatibleLegaoSchema(legaoSchema); + + // Define the expected output + const expectedOutput = { + type: 'JSExpression', + value: 'LegaoCompiled', + extType: 'function', + }; + + // Assert that the result matches the expected output + expect(result).toEqual(expectedOutput); + }); + + // Add more test cases for other scenarios +}); + +describe('getNodeSchemaById', () => { + it('should find a node in the schema', () => { + // Create your test schema and node ID + const testSchema = { + id: 'root', + children: [ + { + id: 'child1', + children: [ + { + id: 'child1.1', + }, + ], + }, + ], + }; + const nodeId = 'child1.1'; + + const result = getNodeSchemaById(testSchema, nodeId); + + // Define the expected output + const expectedOutput = { + id: 'child1.1', + }; + + // Assert that the result matches the expected output + expect(result).toEqual(expectedOutput); + }); + + // Add more test cases for other scenarios +}); + +describe('applyActivities', () => { + it('should apply ADD activity', () => { + // Create your test schema and activities + const testSchema = { + id: 'root', + children: [ + { + id: 'child1', + children: [ + { + id: 'child1.1', + }, + ], + }, + ], + }; + const activities = [ + { + type: ActivityType.ADDED, + payload: { + location: { + parent: { + nodeId: 'child1', + index: 0, + }, + }, + schema: { + id: 'newChild', + }, + }, + }, + ]; + + const result = applyActivities(testSchema, activities); + + // Define the expected output + const expectedOutput = { + id: 'root', + children: [ + { + id: 'child1', + children: [ + { + id: 'newChild', + }, + { + id: 'child1.1', + }, + ], + }, + ], + }; + + // Assert that the result matches the expected output + expect(result).toEqual(expectedOutput); + }); + + // Add more test cases for other activity types and scenarios +}); + + describe('Schema Ut', () => { it('props', () => { const schema = { diff --git a/packages/utils/test/src/script.test.ts b/packages/utils/test/src/script.test.ts new file mode 100644 index 0000000000..d3d4ffd59a --- /dev/null +++ b/packages/utils/test/src/script.test.ts @@ -0,0 +1,47 @@ +import { + evaluate, + evaluateExpression, + newFunction, +} from '../../src/script'; + +describe('evaluate', () => { + test('should evaluate the given script', () => { + const script = 'console.log("Hello, world!");'; + global.console = { log: jest.fn() }; + + evaluate(script); + + expect(global.console.log).toHaveBeenCalledWith('Hello, world!'); + }); +}); + +describe('evaluateExpression', () => { + test('should evaluate the given expression', () => { + const expr = 'return 1 + 2'; + + const result = evaluateExpression(expr); + + expect(result).toBe(3); + }); +}); + +describe('newFunction', () => { + test('should create a new function with the given arguments and code', () => { + const args = 'a, b'; + const code = 'return a + b'; + + const result = newFunction(args, code); + + expect(result).toBeInstanceOf(Function); + expect(result(1, 2)).toBe(3); + }); + + test('should return null if an error occurs', () => { + const args = 'a, b'; + const code = 'return a +;'; // Invalid code + + const result = newFunction(args, code); + + expect(result).toBeNull(); + }); +}); diff --git a/packages/utils/test/src/svg-icon.test.tsx b/packages/utils/test/src/svg-icon.test.tsx new file mode 100644 index 0000000000..bbb6e18b7c --- /dev/null +++ b/packages/utils/test/src/svg-icon.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { SVGIcon, IconProps } from '../../src/svg-icon'; + +describe('SVGIcon', () => { + it('should render SVG element with correct size', () => { + const iconProps: IconProps = { + size: 'small', + viewBox: '0 0 24 24', + }; + + const { container } = render(<SVGIcon {...iconProps} />); + + const svgElement = container.querySelector('svg'); + + expect(svgElement).toHaveAttribute('width', '12'); + expect(svgElement).toHaveAttribute('height', '12'); + }); + + it('should render SVG element with custom size', () => { + const iconProps: IconProps = { + size: 24, + viewBox: '0 0 24 24', + }; + + const { container } = render(<SVGIcon {...iconProps} />); + + const svgElement = container.querySelector('svg'); + + expect(svgElement).toHaveAttribute('width', '24'); + expect(svgElement).toHaveAttribute('height', '24'); + }); + + // Add more tests for other scenarios if needed +}); diff --git a/packages/utils/test/src/transaction-manager.test.ts b/packages/utils/test/src/transaction-manager.test.ts new file mode 100644 index 0000000000..42c7fa8bf0 --- /dev/null +++ b/packages/utils/test/src/transaction-manager.test.ts @@ -0,0 +1,58 @@ +import { transactionManager } from '../../src/transaction-manager'; +import { IPublicEnumTransitionType } from '@alilc/lowcode-types'; + +const type = IPublicEnumTransitionType.REPAINT; + +describe('TransactionManager', () => { + let fn1: jest.Mock; + let fn2: jest.Mock; + + beforeEach(() => { + fn1 = jest.fn(); + fn2 = jest.fn(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('executeTransaction should emit startTransaction and endTransaction events', () => { + const startTransactionSpy = jest.spyOn(transactionManager.emitter, 'emit'); + const endTransactionSpy = jest.spyOn(transactionManager.emitter, 'emit'); + + transactionManager.executeTransaction(() => { + // Perform some action within the transaction + }); + + expect(startTransactionSpy).toHaveBeenCalledWith(`[${type}]startTransaction`); + expect(endTransactionSpy).toHaveBeenCalledWith(`[${type}]endTransaction`); + }); + + test('onStartTransaction should register the provided function for startTransaction event', () => { + const offSpy = jest.spyOn(transactionManager.emitter, 'off'); + + const offFunction = transactionManager.onStartTransaction(fn1); + + expect(transactionManager.emitter.listenerCount(`[${type}]startTransaction`)).toBe(1); + expect(offSpy).not.toHaveBeenCalled(); + + offFunction(); + + expect(transactionManager.emitter.listenerCount(`[${type}]startTransaction`)).toBe(0); + expect(offSpy).toHaveBeenCalledWith(`[${type}]startTransaction`, fn1); + }); + + test('onEndTransaction should register the provided function for endTransaction event', () => { + const offSpy = jest.spyOn(transactionManager.emitter, 'off'); + + const offFunction = transactionManager.onEndTransaction(fn2); + + expect(transactionManager.emitter.listenerCount(`[${type}]endTransaction`)).toBe(1); + expect(offSpy).not.toHaveBeenCalled(); + + offFunction(); + + expect(transactionManager.emitter.listenerCount(`[${type}]endTransaction`)).toBe(0); + expect(offSpy).toHaveBeenCalledWith(`[${type}]endTransaction`, fn2); + }); +}); diff --git a/packages/utils/test/src/unique-id.test.ts b/packages/utils/test/src/unique-id.test.ts new file mode 100644 index 0000000000..2b4b6e9e04 --- /dev/null +++ b/packages/utils/test/src/unique-id.test.ts @@ -0,0 +1,11 @@ +import { uniqueId } from '../../src/unique-id'; + +test('uniqueId should return a unique id with prefix', () => { + const id = uniqueId('test'); + expect(id.startsWith('test')).toBeTruthy(); +}); + +test('uniqueId should return a unique id without prefix', () => { + const id = uniqueId(); + expect(id).not.toBeFalsy(); +}); diff --git a/packages/workspace/build.json b/packages/workspace/build.json new file mode 100644 index 0000000000..3e92600554 --- /dev/null +++ b/packages/workspace/build.json @@ -0,0 +1,5 @@ +{ + "plugins": [ + "@alilc/build-plugin-lce" + ] +} diff --git a/packages/workspace/build.test.json b/packages/workspace/build.test.json new file mode 100644 index 0000000000..9cc30d7463 --- /dev/null +++ b/packages/workspace/build.test.json @@ -0,0 +1,6 @@ +{ + "plugins": [ + "@alilc/build-plugin-lce", + "@alilc/lowcode-test-mate/plugin/index.ts" + ] +} diff --git a/packages/workspace/jest.config.js b/packages/workspace/jest.config.js new file mode 100644 index 0000000000..0e05687d78 --- /dev/null +++ b/packages/workspace/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], + collectCoverage: true, + collectCoverageFrom: [ + 'src/**/*.{ts,tsx}', + '!**/node_modules/**', + '!**/vendor/**', + ], +}; diff --git a/packages/workspace/package.json b/packages/workspace/package.json new file mode 100644 index 0000000000..778b8167f8 --- /dev/null +++ b/packages/workspace/package.json @@ -0,0 +1,55 @@ +{ + "name": "@alilc/lowcode-workspace", + "version": "1.3.2", + "description": "Shell Layer for AliLowCodeEngine", + "main": "lib/index.js", + "module": "es/index.js", + "files": [ + "lib", + "es" + ], + "scripts": { + "build": "build-scripts build", + "test": "build-scripts test --config build.test.json", + "test:cov": "build-scripts test --config build.test.json --jest-coverage" + }, + "license": "MIT", + "dependencies": { + "@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", + "react": "^16", + "react-dom": "^16.7.0" + }, + "devDependencies": { + "@alib/build-scripts": "^0.1.29", + "@testing-library/react": "^11.2.2", + "@types/classnames": "^2.2.7", + "@types/jest": "^26.0.16", + "@types/lodash": "^4.14.165", + "@types/medium-editor": "^5.0.3", + "@types/node": "^13.7.1", + "@types/react": "^16", + "@types/react-dom": "^16", + "jest": "^26.6.3", + "lodash": "^4.17.20", + "moment": "^2.29.1", + "typescript": "^4.0.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + }, + "repository": { + "type": "http", + "url": "https://github.com/alibaba/lowcode-engine/tree/main/packages/workspace" + }, + "gitHead": "2669f179e6f899d395ce1942d0fe04f9c5ed48a6", + "bugs": "https://github.com/alibaba/lowcode-engine/issues", + "homepage": "https://github.com/alibaba/lowcode-engine/#readme" +} diff --git a/packages/workspace/src/context/base-context.ts b/packages/workspace/src/context/base-context.ts new file mode 100644 index 0000000000..445677a618 --- /dev/null +++ b/packages/workspace/src/context/base-context.ts @@ -0,0 +1,200 @@ +import { + Editor, + engineConfig, Setters as InnerSetters, + Hotkey as InnerHotkey, + commonEvent, + IEngineConfig, + IHotKey, + Command as InnerCommand, +} from '@alilc/lowcode-editor-core'; +import { + Designer, + ILowCodePluginContextApiAssembler, + LowCodePluginManager, + ILowCodePluginContextPrivate, + IProject, + IDesigner, + ILowCodePluginManager, +} from '@alilc/lowcode-designer'; +import { + ISkeleton, + Skeleton as InnerSkeleton, +} from '@alilc/lowcode-editor-skeleton'; +import { + Hotkey, + Plugins, + Project, + Skeleton, + Setters, + Material, + Event, + Common, + Logger, + Workspace, + Window, + Canvas, + CommonUI, + Command, +} from '@alilc/lowcode-shell'; +import { + IPluginPreferenceMananger, + IPublicApiCanvas, + IPublicApiCommon, + IPublicApiEvent, + IPublicApiHotkey, + IPublicApiMaterial, + IPublicApiPlugins, + IPublicApiProject, + IPublicApiSetters, + IPublicApiSkeleton, + IPublicEnumPluginRegisterLevel, + IPublicModelPluginContext, + IPublicTypePluginMeta, +} from '@alilc/lowcode-types'; +import { getLogger, Logger as InnerLogger } from '@alilc/lowcode-utils'; +import { IWorkspace } from '../workspace'; +import { IEditorWindow } from '../window'; + +export interface IBasicContext extends Omit<IPublicModelPluginContext, 'workspace'> { + skeleton: IPublicApiSkeleton; + plugins: IPublicApiPlugins; + project: IPublicApiProject; + setters: IPublicApiSetters; + material: IPublicApiMaterial; + common: IPublicApiCommon; + config: IEngineConfig; + event: IPublicApiEvent; + logger: InnerLogger; + hotkey: IPublicApiHotkey; + innerProject: IProject; + editor: Editor; + designer: IDesigner; + registerInnerPlugins: () => Promise<void>; + innerSetters: InnerSetters; + innerSkeleton: ISkeleton; + innerHotkey: IHotKey; + innerPlugins: ILowCodePluginManager; + canvas: IPublicApiCanvas; + pluginEvent: IPublicApiEvent; + preference: IPluginPreferenceMananger; + workspace: IWorkspace; +} + +export class BasicContext implements IBasicContext { + skeleton: IPublicApiSkeleton; + plugins: IPublicApiPlugins; + project: IPublicApiProject; + setters: IPublicApiSetters; + material: IPublicApiMaterial; + common: IPublicApiCommon; + config: IEngineConfig; + event: IPublicApiEvent; + logger: InnerLogger; + hotkey: IPublicApiHotkey; + innerProject: IProject; + editor: Editor; + designer: IDesigner; + registerInnerPlugins: () => Promise<void>; + innerSetters: InnerSetters; + innerSkeleton: ISkeleton; + innerHotkey: IHotKey; + innerPlugins: ILowCodePluginManager; + canvas: IPublicApiCanvas; + pluginEvent: IPublicApiEvent; + preference: IPluginPreferenceMananger; + workspace: IWorkspace; + + constructor(innerWorkspace: IWorkspace, viewName: string, readonly registerLevel: IPublicEnumPluginRegisterLevel, public editorWindow?: IEditorWindow) { + const editor = new Editor(viewName, true); + + const innerSkeleton = new InnerSkeleton(editor, viewName); + editor.set('skeleton' as any, innerSkeleton); + + const designer: Designer = new Designer({ + editor, + viewName, + shellModelFactory: innerWorkspace?.shellModelFactory, + }); + editor.set('designer' as any, designer); + + const { project: innerProject } = designer; + const workspace = new Workspace(innerWorkspace); + const innerHotkey = new InnerHotkey(viewName); + const hotkey = new Hotkey(innerHotkey, true); + const innerSetters = new InnerSetters(viewName); + const setters = new Setters(innerSetters, true); + const material = new Material(editor, true); + const project = new Project(innerProject, true); + const config = engineConfig; + const event = new Event(commonEvent, { prefix: 'common' }); + const logger = getLogger({ level: 'warn', bizName: 'common' }); + 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); + editor.set('hotkey', hotkey); + editor.set('innerHotkey', innerHotkey); + this.innerSetters = innerSetters; + this.innerSkeleton = innerSkeleton; + this.skeleton = skeleton; + this.innerProject = innerProject; + this.project = project; + this.setters = setters; + this.material = material; + this.config = config; + this.event = event; + this.logger = logger; + this.hotkey = hotkey; + this.innerHotkey = innerHotkey; + this.editor = editor; + this.designer = designer; + this.canvas = canvas; + const common = new Common(editor, innerSkeleton); + this.common = common; + let plugins: IPublicApiPlugins; + + const pluginContextApiAssembler: ILowCodePluginContextApiAssembler = { + assembleApis: (context: ILowCodePluginContextPrivate, pluginName: string, meta: IPublicTypePluginMeta) => { + context.workspace = workspace; + context.hotkey = hotkey; + context.project = project; + context.skeleton = new Skeleton(innerSkeleton, pluginName, true); + 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; + context.plugins = plugins; + context.logger = new Logger({ level: 'warn', bizName: `plugin:${pluginName}` }); + context.canvas = canvas; + context.commonUI = commonUI; + 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); + }, + }; + + const innerPlugins = new LowCodePluginManager(pluginContextApiAssembler, viewName); + this.innerPlugins = innerPlugins; + plugins = new Plugins(innerPlugins, true).toProxy(); + editor.set('plugins' as any, plugins); + editor.set('innerPlugins' as any, innerPlugins); + this.plugins = plugins; + + // 注册一批内置插件 + this.registerInnerPlugins = async function registerPlugins() { + await innerWorkspace?.registryInnerPlugin(designer, editor, plugins); + }; + } +} \ No newline at end of file diff --git a/packages/workspace/src/context/view-context.ts b/packages/workspace/src/context/view-context.ts new file mode 100644 index 0000000000..0542f83a95 --- /dev/null +++ b/packages/workspace/src/context/view-context.ts @@ -0,0 +1,70 @@ +import { computed, makeObservable, obx } from '@alilc/lowcode-editor-core'; +import { IPublicEditorViewConfig, IPublicEnumPluginRegisterLevel, IPublicTypeEditorView } from '@alilc/lowcode-types'; +import { flow } from 'mobx'; +import { IWorkspace } from '../workspace'; +import { BasicContext, IBasicContext } from './base-context'; +import { IEditorWindow } from '../window'; +import { getWebviewPlugin } from '../inner-plugins/webview'; + +export interface IViewContext extends IBasicContext { + editorWindow: IEditorWindow; + + viewName: string; + + viewType: 'editor' | 'webview'; +} + +export class Context extends BasicContext implements IViewContext { + viewName = 'editor-view'; + + instance: IPublicEditorViewConfig; + + viewType: 'editor' | 'webview'; + + @obx _activate = false; + + @obx isInit: boolean = false; + + init = flow(function* (this: Context) { + if (this.viewType === 'webview') { + const url = yield this.instance?.url?.(); + yield this.plugins.register(getWebviewPlugin(url, this.viewName)); + } else { + yield this.registerInnerPlugins(); + } + yield this.instance?.init?.(); + yield this.innerPlugins.init(); + this.isInit = true; + }); + + constructor(public workspace: IWorkspace, public editorWindow: IEditorWindow, public editorView: IPublicTypeEditorView, options: Object | undefined) { + super(workspace, editorView.viewName, IPublicEnumPluginRegisterLevel.EditorView, editorWindow); + this.viewType = editorView.viewType || 'editor'; + this.viewName = editorView.viewName; + this.instance = editorView(this.innerPlugins._getLowCodePluginContext({ + pluginName: 'any', + }), options); + makeObservable(this); + } + + @computed get active() { + return this._activate; + } + + onSimulatorRendererReady = (): Promise<void> => { + return new Promise((resolve) => { + this.project.onSimulatorRendererReady(() => { + resolve(); + }); + }); + }; + + setActivate = (_activate: boolean) => { + this._activate = _activate; + this.innerHotkey.activate(this._activate); + }; + + async save() { + return await this.instance?.save?.(); + } +} \ No newline at end of file diff --git a/packages/workspace/src/index.ts b/packages/workspace/src/index.ts new file mode 100644 index 0000000000..6c437fad0a --- /dev/null +++ b/packages/workspace/src/index.ts @@ -0,0 +1,7 @@ +export { Workspace } from './workspace'; +export type { IWorkspace } from './workspace'; +export * from './window'; +export * from './layouts/workbench'; +export { Resource } from './resource'; +export type { IResource } from './resource'; +export type { IViewContext } from './context/view-context'; diff --git a/packages/workspace/src/inner-plugins/webview.tsx b/packages/workspace/src/inner-plugins/webview.tsx new file mode 100644 index 0000000000..820b843ab8 --- /dev/null +++ b/packages/workspace/src/inner-plugins/webview.tsx @@ -0,0 +1,49 @@ +import { IPublicModelPluginContext } from '@alilc/lowcode-types'; + +export function DesignerView(props: { + url: string; + viewName?: string; +}) { + return ( + <div className="lc-designer lowcode-plugin-designer"> + <div className="lc-project"> + <div className="lc-simulator-shell"> + <iframe + name={`webview-view-${props.viewName}`} + className="lc-simulator-content-frame" + style={{ + height: '100%', + width: '100%', + }} + src={props.url} + /> + </div> + </div> + </div> + ); +} + +export function getWebviewPlugin(url: string, viewName: string) { + function webviewPlugin(ctx: IPublicModelPluginContext) { + const { skeleton } = ctx; + return { + init() { + skeleton.add({ + area: 'mainArea', + name: 'designer', + type: 'Widget', + content: DesignerView, + contentProps: { + ctx, + url, + viewName, + }, + }); + }, + }; + } + + webviewPlugin.pluginName = '___webview_plugin___'; + + return webviewPlugin; +} diff --git a/packages/workspace/src/layouts/workbench.tsx b/packages/workspace/src/layouts/workbench.tsx new file mode 100644 index 0000000000..2913576e1c --- /dev/null +++ b/packages/workspace/src/layouts/workbench.tsx @@ -0,0 +1,84 @@ +import { Component } from 'react'; +import { TipContainer, engineConfig, observer } from '@alilc/lowcode-editor-core'; +import { WindowView } from '../view/window-view'; +import classNames from 'classnames'; +import { SkeletonContext } from '../skeleton-context'; +import { EditorConfig, PluginClassSet } from '@alilc/lowcode-types'; +import { Workspace } from '../workspace'; +import { BottomArea, LeftArea, LeftFixedPane, LeftFloatPane, MainArea, SubTopArea, TopArea } from '@alilc/lowcode-editor-skeleton'; + +@observer +export class Workbench extends Component<{ + workspace: Workspace; + config?: EditorConfig; + components?: PluginClassSet; + className?: string; + topAreaItemClassName?: string; +}, { + workspaceEmptyComponent: any; + theme?: string; +}> { + constructor(props: any) { + super(props); + const { config, components, workspace } = this.props; + const { skeleton } = workspace; + skeleton.buildFromConfig(config, components); + engineConfig.onGot('theme', (theme) => { + this.setState({ + theme, + }); + }); + engineConfig.onGot('workspaceEmptyComponent', (workspaceEmptyComponent) => { + this.setState({ + workspaceEmptyComponent, + }); + }); + this.state = { + workspaceEmptyComponent: engineConfig.get('workspaceEmptyComponent'), + theme: engineConfig.get('theme'), + }; + } + + render() { + const { workspace, className, topAreaItemClassName } = this.props; + const { skeleton } = workspace; + const { workspaceEmptyComponent: WorkspaceEmptyComponent, theme } = this.state; + + return ( + <div className={classNames('lc-workspace-workbench', className, theme)}> + <SkeletonContext.Provider value={skeleton}> + <TopArea className="lc-workspace-top-area" area={skeleton.topArea} itemClassName={topAreaItemClassName} /> + <div className="lc-workspace-workbench-body"> + <LeftArea className="lc-workspace-left-area lc-left-area" area={skeleton.leftArea} /> + <LeftFloatPane area={skeleton.leftFloatArea} /> + <LeftFixedPane area={skeleton.leftFixedArea} /> + <div className="lc-workspace-workbench-center"> + <div className="lc-workspace-workbench-center-content"> + <SubTopArea area={skeleton.subTopArea} itemClassName={topAreaItemClassName} /> + <div className="lc-workspace-workbench-window"> + { + workspace.windows.map(d => ( + <WindowView + active={d.id === workspace.window?.id} + window={d} + key={d.id} + /> + )) + } + + { + !workspace.windows.length && WorkspaceEmptyComponent ? <WorkspaceEmptyComponent /> : null + } + </div> + </div> + <MainArea area={skeleton.mainArea} /> + <BottomArea area={skeleton.bottomArea} /> + </div> + {/* <RightArea area={skeleton.rightArea} /> */} + </div> + <TipContainer /> + </SkeletonContext.Provider> + </div> + ); + } +} diff --git a/packages/workspace/src/less-variables.less b/packages/workspace/src/less-variables.less new file mode 100644 index 0000000000..017e432ce6 --- /dev/null +++ b/packages/workspace/src/less-variables.less @@ -0,0 +1,215 @@ +/* + * 基础的 DPL 定义使用了 kuma base 的定义,参考: + * https://github.com/uxcore/kuma-base/tree/master/variables + */ + +/** + * =========================================================== + * ==================== Font Family ========================== + * =========================================================== + */ + +/* + * @font-family: "STHeiti", "Microsoft Yahei", "Lucida Grande", "Lucida Sans Unicode", Helvetica, Arial, Verdana, sans-serif; + */ + +@font-family: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, sans-serif; +@font-family-code: Monaco, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Helvetica, Arial, + sans-serif; + +/** + * =========================================================== + * ===================== Color DPL =========================== + * =========================================================== + */ + +@brand-color-1: rgba(0, 108, 255, 1); +@brand-color-2: rgba(25, 122, 255, 1); +@brand-color-3: rgba(0, 96, 229, 1); + +@brand-color-1-3: rgba(0, 108, 255, 0.6); +@brand-color-1-4: rgba(0, 108, 255, 0.4); +@brand-color-1-5: rgba(0, 108, 255, 0.3); +@brand-color-1-6: rgba(0, 108, 255, 0.2); +@brand-color-1-7: rgba(0, 108, 255, 0.1); + +@brand-color: @brand-color-1; + +@white-alpha-1: rgb(255, 255, 255); // W-1 +@white-alpha-2: rgba(255, 255, 255, 0.8); // W-2 A80 +@white-alpha-3: rgba(255, 255, 255, 0.6); // W-3 A60 +@white-alpha-4: rgba(255, 255, 255, 0.4); // W-4 A40 +@white-alpha-5: rgba(255, 255, 255, 0.3); // W-5 A30 +@white-alpha-6: rgba(255, 255, 255, 0.2); // W-6 A20 +@white-alpha-7: rgba(255, 255, 255, 0.1); // W-7 A10 +@white-alpha-8: rgba(255, 255, 255, 0.06); // W-8 A6 + +@dark-alpha-1: rgba(0, 0, 0, 1); // D-1 A100 +@dark-alpha-2: rgba(0, 0, 0, 0.8); // D-2 A80 +@dark-alpha-3: rgba(0, 0, 0, 0.6); // D-3 A60 +@dark-alpha-4: rgba(0, 0, 0, 0.4); // D-4 A40 +@dark-alpha-5: rgba(0, 0, 0, 0.3); // D-5 A30 +@dark-alpha-6: rgba(0, 0, 0, 0.2); // D-6 A20 +@dark-alpha-7: rgba(0, 0, 0, 0.1); // D-7 A10 +@dark-alpha-8: rgba(0, 0, 0, 0.06); // D-8 A6 +@dark-alpha-9: rgba(0, 0, 0, 0.04); // D-9 A4 + +@normal-alpha-1: rgba(31, 56, 88, 1); // N-1 A100 +@normal-alpha-2: rgba(31, 56, 88, 0.8); // N-2 A80 +@normal-alpha-3: rgba(31, 56, 88, 0.6); // N-3 A60 +@normal-alpha-4: rgba(31, 56, 88, 0.4); // N-4 A40 +@normal-alpha-5: rgba(31, 56, 88, 0.3); // N-5 A30 +@normal-alpha-6: rgba(31, 56, 88, 0.2); // N-6 A20 +@normal-alpha-7: rgba(31, 56, 88, 0.1); // N-7 A10 +@normal-alpha-8: rgba(31, 56, 88, 0.06); // N-8 A6 +@normal-alpha-9: rgba(31, 56, 88, 0.04); // N-9 A4 + +@normal-3: #77879c; +@normal-4: #a3aebd; +@normal-5: #bac3cc; +@normal-6: #d1d7de; + +@gray-dark: #333; // N2_4 +@gray: #666; // N2_3 +@gray-light: #999; // N2_2 +@gray-lighter: #ccc; // N2_1 + +@brand-secondary: #2c2f33; // B2_3 +// 补色 +@brand-complement: #00b3e8; // B3_1 +// 复合 +@brand-comosite: #00c587; // B3_2 +// 浓度 +@brand-deep: #73461d; // B3_3 + +// F1-1 +@brand-danger: rgb(240, 70, 49); +// F1-2 (10% white) +@brand-danger-hover: rgba(240, 70, 49, 0.9); +// F1-3 (5% black) +@brand-danger-focus: rgba(240, 70, 49, 0.95); + +// F2-1 +@brand-warning: rgb(250, 189, 14); +// F3-1 +@brand-success: rgb(102, 188, 92); +// F4-1 +@brand-link: rgb(102, 188, 92); +// F4-2 +@brand-link-hover: #2e76a6; + +// F1-1-7 A10 +@brand-danger-alpha-7: rgba(240, 70, 49, 0.1); +// F1-1-8 A6 +@brand-danger-alpha-8: rgba(240, 70, 49, 0.8); +// F2-1-2 A80 +@brand-warning-alpha-2: rgba(250, 189, 14, 0.8); +// F2-1-7 A10 +@brand-warning-alpha-7: rgba(250, 189, 14, 0.1); +// F3-1-2 A80 +@brand-success-alpha-2: rgba(102, 188, 92, 0.8); +// F3-1-7 A10 +@brand-success-alpha-7: rgba(102, 188, 92, 0.1); +// F4-1-7 A10 +@brand-link-alpha-7: rgba(102, 188, 92, 0.1); + +// 文本色 +@text-primary-color: @dark-alpha-3; +@text-secondary-color: @normal-alpha-3; +@text-thirdary-color: @dark-alpha-4; +@text-disabled-color: @normal-alpha-5; +@text-helper-color: @dark-alpha-4; +@text-danger-color: @brand-danger; +@text-ali-color: #ec6c00; + +/** + * =========================================================== + * =================== Shadow Box ============================ + * =========================================================== + */ + +@box-shadow-1: 0 1px 4px 0 rgba(31, 56, 88, 0.15); // 1 级阴影,物体由原来存在于底面的物体展开,物体和底面关联紧密 +@box-shadow-2: 0 2px 10px 0 rgba(31, 56, 88, 0.15); // 2 级阴影,hover状态,物体层级较高 +@box-shadow-3: 0 4px 15px 0 rgba(31, 56, 88, 0.15); // 3 级阴影,当物体层级高于所有界面元素,弹窗用 + +/** + * =========================================================== + * ================= FontSize of Level ======================= + * =========================================================== + */ + +@fontSize-1: 26px; +@fontSize-2: 20px; +@fontSize-3: 16px; +@fontSize-4: 14px; +@fontSize-5: 12px; + +@fontLineHeight-1: 38px; +@fontLineHeight-2: 30px; +@fontLineHeight-3: 26px; +@fontLineHeight-4: 24px; +@fontLineHeight-5: 20px; + +/** + * =========================================================== + * ================= FontSize of Level ======================= + * =========================================================== + */ + +@global-border-radius: 3px; +@input-border-radius: 3px; +@popup-border-radius: 6px; + +/** + * =========================================================== + * ===================== Transistion ========================= + * =========================================================== + */ + +@transition-duration: 0.3s; +@transition-ease: cubic-bezier(0.23, 1, 0.32, 1); +@transition-delay: 0s; + +/** + * =========================================================== + * ================ Global Configruations ==================== + * =========================================================== + */ + +@topPaneHeight: 48px; +@actionpane-height: 48px; +@tabPaneWidth: 260px; +@input-standard-height: 32px; +@dockpane-width: 48px; + +/** + * =========================================================== + * =================== Deprecated Items ====================== + * =========================================================== + */ + +@head-bgcolor: @white-alpha-1; +@pane-bgcolor: @white-alpha-1; +@pane-dark-bgcolor: @white-alpha-1; +@pane-bdcolor: @normal-4; +@blank-bgcolor: @normal-5; +@title-bgcolor: @white-alpha-1; +@title-bdcolor: transparent; +@section-bgcolor: transparent; +@section-bdcolor: @white-alpha-1; +@button-bgcolor: @white-alpha-1; +@button-bdcolor: transparent; +@button-blue-color: @brand-color; +@button-blue-hover-color: @brand-color; +@sub-title-bgcolor: @white-alpha-1; +@sub-title-bdcolor: transparent; +@text-color: @text-primary-color; +@icon-color: @gray; +@icon-color-active: @gray-light; +@ghost-bgcolor: @dark-alpha-3; +@input-bgcolor: transparent; +@input-bdcolor: @normal-alpha-5; +@hover-color: #5a99cc; +@active-color: #5a99cc; +@disabled-color: #666; +@setter-popup-bg: rgb(80, 86, 109); diff --git a/packages/workspace/src/resource-type.ts b/packages/workspace/src/resource-type.ts new file mode 100644 index 0000000000..28d54e56b3 --- /dev/null +++ b/packages/workspace/src/resource-type.ts @@ -0,0 +1,22 @@ +import { IPublicTypeResourceType } from '@alilc/lowcode-types'; + +export interface IResourceType extends Omit<IPublicTypeResourceType, 'resourceName' | 'resourceType'> { + name: string; + + type: 'editor' | 'webview'; + + resourceTypeModel: IPublicTypeResourceType; +} + +export class ResourceType implements IResourceType { + constructor(readonly resourceTypeModel: IPublicTypeResourceType) { + } + + get name() { + return this.resourceTypeModel.resourceName; + } + + get type() { + return this.resourceTypeModel.resourceType; + } +} \ No newline at end of file diff --git a/packages/workspace/src/resource.ts b/packages/workspace/src/resource.ts new file mode 100644 index 0000000000..6e85183853 --- /dev/null +++ b/packages/workspace/src/resource.ts @@ -0,0 +1,130 @@ +import { ISkeleton } from '@alilc/lowcode-editor-skeleton'; +import { IPublicTypeEditorView, IPublicResourceData, IPublicResourceTypeConfig, IBaseModelResource, IPublicEnumPluginRegisterLevel } from '@alilc/lowcode-types'; +import { Logger } from '@alilc/lowcode-utils'; +import { BasicContext, IBasicContext } from './context/base-context'; +import { ResourceType, IResourceType } from './resource-type'; +import { IWorkspace } from './workspace'; + +const logger = new Logger({ level: 'warn', bizName: 'workspace:resource' }); + +export interface IBaseResource<T> extends IBaseModelResource<T> { + readonly resourceType: ResourceType; + + skeleton: ISkeleton; + + description?: string; + + get editorViews(): IPublicTypeEditorView[]; + + get defaultViewName(): string | undefined; + + getEditorView(name: string): IPublicTypeEditorView | undefined; + + import(schema: any): Promise<any>; + + save(value: any): Promise<any>; + + url(): Promise<string | undefined>; +} + +export type IResource = IBaseResource<IResource>; + +export class Resource implements IResource { + private context: IBasicContext; + + resourceTypeInstance: IPublicResourceTypeConfig; + + editorViewMap: Map<string, IPublicTypeEditorView> = new Map<string, IPublicTypeEditorView>(); + + get name() { + return this.resourceType.name; + } + + get viewName() { + return this.resourceData.viewName || (this.resourceData as any).viewType || this.defaultViewName; + } + + get description() { + return this.resourceTypeInstance?.description; + } + + get icon() { + return this.resourceData.icon || this.resourceTypeInstance?.icon; + } + + get type() { + return this.resourceType.type; + } + + get title(): string | undefined { + return this.resourceData.title || this.resourceTypeInstance.defaultTitle; + } + + get id(): string | undefined { + return this.resourceData.id; + } + + get options() { + return this.resourceData.options; + } + + get category() { + return this.resourceData?.category; + } + + get skeleton() { + return this.context.innerSkeleton; + } + + children: IResource[]; + + get config() { + return this.resourceData.config; + } + + constructor(readonly resourceData: IPublicResourceData, readonly resourceType: IResourceType, readonly workspace: IWorkspace) { + this.context = new BasicContext(workspace, `resource-${resourceData.resourceName || resourceType.name}`, IPublicEnumPluginRegisterLevel.Resource); + this.resourceTypeInstance = resourceType.resourceTypeModel(this.context.innerPlugins._getLowCodePluginContext({ + pluginName: '', + }), this.options); + this.init(); + if (this.resourceTypeInstance.editorViews) { + this.resourceTypeInstance.editorViews.forEach((d: any) => { + this.editorViewMap.set(d.viewName, d); + }); + } + if (!resourceType) { + logger.error(`resourceType[${resourceType}] is unValid.`); + } + this.children = this.resourceData?.children?.map(d => new Resource(d, this.workspace.getResourceType(d.resourceName || this.resourceType.name), this.workspace)) || []; + } + + async init() { + await this.resourceTypeInstance.init?.(); + await this.context.innerPlugins.init(); + } + + async import(schema: any) { + return await this.resourceTypeInstance.import?.(schema); + } + + async url() { + return await this.resourceTypeInstance.url?.(); + } + + async save(value: any) { + return await this.resourceTypeInstance.save?.(value); + } + + get editorViews() { + return this.resourceTypeInstance.editorViews; + } + + get defaultViewName() { + return this.resourceTypeInstance.defaultViewName || this.resourceTypeInstance.defaultViewType; + } + + getEditorView(name: string) { + return this.editorViewMap.get(name); + } +} \ No newline at end of file diff --git a/packages/workspace/src/skeleton-context.ts b/packages/workspace/src/skeleton-context.ts new file mode 100644 index 0000000000..781ba9ae8c --- /dev/null +++ b/packages/workspace/src/skeleton-context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'react'; + +export const SkeletonContext = createContext<any>({} as any); diff --git a/packages/workspace/src/view/editor-view.tsx b/packages/workspace/src/view/editor-view.tsx new file mode 100644 index 0000000000..7ada5c911e --- /dev/null +++ b/packages/workspace/src/view/editor-view.tsx @@ -0,0 +1,33 @@ +import { BuiltinLoading } from '@alilc/lowcode-designer'; +import { engineConfig, observer } from '@alilc/lowcode-editor-core'; +import { + Workbench, +} from '@alilc/lowcode-editor-skeleton'; +import { PureComponent } from 'react'; +import { Context } from '../context/view-context'; + +export * from '../context/base-context'; + +@observer +export class EditorView extends PureComponent<{ + editorView: Context; + active: boolean; +}, any> { + render() { + const { active } = this.props; + const editorView = this.props.editorView; + const skeleton = editorView.innerSkeleton; + if (!editorView.isInit) { + const Loading = engineConfig.get('loadingComponent', BuiltinLoading); + return <Loading />; + } + + return ( + <Workbench + skeleton={skeleton} + className={active ? 'active engine-editor-view' : 'engine-editor-view'} + topAreaItemClassName="engine-actionitem" + /> + ); + } +} diff --git a/packages/workspace/src/view/resource-view.less b/packages/workspace/src/view/resource-view.less new file mode 100644 index 0000000000..4c281f8d8f --- /dev/null +++ b/packages/workspace/src/view/resource-view.less @@ -0,0 +1,14 @@ +.workspace-resource-view { + display: flex; + position: absolute; + flex-direction: column; + top: 0; + bottom: 0; + left: 0; + right: 0; +} + +.workspace-editor-body { + position: relative; + height: 100%; +} \ No newline at end of file diff --git a/packages/workspace/src/view/resource-view.tsx b/packages/workspace/src/view/resource-view.tsx new file mode 100644 index 0000000000..e2204dd505 --- /dev/null +++ b/packages/workspace/src/view/resource-view.tsx @@ -0,0 +1,36 @@ +import { PureComponent } from 'react'; +import { EditorView } from './editor-view'; +import { observer } from '@alilc/lowcode-editor-core'; +import { IResource } from '../resource'; +import { IEditorWindow } from '../window'; +import './resource-view.less'; +import { TopArea } from '@alilc/lowcode-editor-skeleton'; + +@observer +export class ResourceView extends PureComponent<{ + window: IEditorWindow; + resource: IResource; +}, any> { + render() { + const { skeleton } = this.props.resource; + const { editorViews } = this.props.window; + return ( + <div className="workspace-resource-view"> + <TopArea area={skeleton.topArea} itemClassName="engine-actionitem" /> + <div className="workspace-editor-body"> + { + Array.from(editorViews.values()).map((editorView: any) => { + return ( + <EditorView + key={editorView.name} + active={editorView.active} + editorView={editorView} + /> + ); + }) + } + </div> + </div> + ); + } +} \ No newline at end of file diff --git a/packages/workspace/src/view/window-view.tsx b/packages/workspace/src/view/window-view.tsx new file mode 100644 index 0000000000..65378bc9c4 --- /dev/null +++ b/packages/workspace/src/view/window-view.tsx @@ -0,0 +1,39 @@ +import { PureComponent } from 'react'; +import { ResourceView } from './resource-view'; +import { engineConfig, observer } from '@alilc/lowcode-editor-core'; +import { EditorWindow } from '../window'; +import { BuiltinLoading } from '@alilc/lowcode-designer'; +import { DesignerView } from '../inner-plugins/webview'; + +@observer +export class WindowView extends PureComponent<{ + window: EditorWindow; + active: boolean; +}, any> { + render() { + const { active } = this.props; + const { resource, initReady, url } = this.props.window; + + if (!initReady) { + const Loading = engineConfig.get('loadingComponent', BuiltinLoading); + return ( + <div className={`workspace-engine-main ${active ? 'active' : ''}`}> + <Loading /> + </div> + ); + } + + if (resource.type === 'webview' && url) { + return <DesignerView url={url} viewName={resource.name} />; + } + + return ( + <div className={`workspace-engine-main ${active ? 'active' : ''}`}> + <ResourceView + resource={resource} + window={this.props.window} + /> + </div> + ); + } +} \ No newline at end of file diff --git a/packages/workspace/src/window.ts b/packages/workspace/src/window.ts new file mode 100644 index 0000000000..cd64a9b112 --- /dev/null +++ b/packages/workspace/src/window.ts @@ -0,0 +1,253 @@ +import { uniqueId } from '@alilc/lowcode-utils'; +import { createModuleEventBus, IEventBus, makeObservable, obx } from '@alilc/lowcode-editor-core'; +import { Context, IViewContext } from './context/view-context'; +import { IWorkspace } from './workspace'; +import { IResource } from './resource'; +import { IPublicModelWindow, IPublicTypeDisposable } from '@alilc/lowcode-types'; + +interface IWindowCOnfig { + title: string | undefined; + options?: Object; + viewName?: string | undefined; + sleep?: boolean; +} + +export interface IEditorWindow extends Omit<IPublicModelWindow<IResource>, 'changeViewType' | 'currentEditorView' | 'editorViews'> { + readonly resource: IResource; + + editorViews: Map<string, IViewContext>; + + _editorView: IViewContext; + + changeViewName: (name: string, ignoreEmit?: boolean) => void; + + initReady: boolean; + + sleep?: boolean; + + init(): void; + + updateState(state: WINDOW_STATE): void; +} + +export enum WINDOW_STATE { + // 睡眠 + sleep = 'sleep', + + // 激活 + active = 'active', + + // 未激活 + inactive = 'inactive', + + // 销毁 + destroyed = 'destroyed' +} + +export class EditorWindow implements IEditorWindow { + id: string = uniqueId('window'); + icon: React.ReactElement | undefined; + + private emitter: IEventBus = createModuleEventBus('Project'); + + title: string | undefined; + + url: string | undefined; + + @obx.ref _editorView: Context; + + @obx editorViews: Map<string, Context> = new Map<string, Context>(); + + @obx initReady = false; + + 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; + this.icon = resource.icon; + this.sleep = config.sleep; + if (config.sleep) { + this.updateState(WINDOW_STATE.sleep); + } + } + + updateState(state: WINDOW_STATE): void { + switch (state) { + case WINDOW_STATE.active: + this._editorView?.setActivate(true); + break; + case WINDOW_STATE.inactive: + this._editorView?.setActivate(false); + break; + case WINDOW_STATE.destroyed: + break; + } + } + + async importSchema(schema: any) { + const newSchema = await this.resource.import(schema); + + if (!newSchema) { + return; + } + + Object.keys(newSchema).forEach(key => { + const view = this.editorViews.get(key); + view?.project.importSchema(newSchema[key]); + }); + } + + async save() { + const value: any = {}; + const editorViews = this.resource.editorViews; + if (!editorViews) { + return; + } + for (let i = 0; i < editorViews.length; i++) { + const name = editorViews[i].viewName; + const saveResult = await this.editorViews.get(name)?.save(); + value[name] = saveResult; + } + const result = await this.resource.save(value); + this.emitter.emit('handle.save'); + + return result; + } + + onSave(fn: () => void) { + this.emitter.on('handle.save', fn); + + return () => { + this.emitter.off('handle.save', fn); + }; + } + + async init() { + await this.initViewTypes(); + await this.execViewTypesInit(); + Promise.all(Array.from(this.editorViews.values()).map((d) => d.onSimulatorRendererReady())) + .then(() => { + this.workspace.emitWindowRendererReady(); + }); + this.url = await this.resource.url(); + this.setDefaultViewName(); + this.initReady = true; + this.workspace.checkWindowQueue(); + this.sleep = false; + this.updateState(WINDOW_STATE.active); + } + + initViewTypes = async () => { + const editorViews = this.resource.editorViews; + if (!editorViews) { + return; + } + for (let i = 0; i < editorViews.length; i++) { + const name = editorViews[i].viewName; + await this.initViewType(name); + if (!this._editorView) { + this.changeViewName(name); + } + } + }; + + onChangeViewType(fn: (viewName: string) => void): IPublicTypeDisposable { + this.emitter.on('window.change.view.type', fn); + + return () => { + this.emitter.off('window.change.view.type', fn); + }; + } + + execViewTypesInit = async () => { + const editorViews = this.resource.editorViews; + if (!editorViews) { + return; + } + for (let i = 0; i < editorViews.length; i++) { + const name = editorViews[i].viewName; + this.changeViewName(name); + await this.editorViews.get(name)?.init(); + } + }; + + setDefaultViewName = () => { + this.changeViewName(this.config.viewName ?? this.resource.defaultViewName!); + }; + + get resourceType() { + return this.resource.resourceType.type; + } + + initViewType = async (name: string) => { + const viewInfo = this.resource.getEditorView(name); + if (this.editorViews.get(name)) { + return; + } + const editorView = new Context(this.workspace, this, viewInfo as any, this.config.options); + this.editorViews.set(name, editorView); + }; + + changeViewName = (name: string, ignoreEmit: boolean = true) => { + this._editorView?.setActivate(false); + this._editorView = this.editorViews.get(name)!; + + if (!this._editorView) { + return; + } + + this._editorView.setActivate(true); + + if (!ignoreEmit) { + this.emitter.emit('window.change.view.type', name); + + if (this.id === this.workspace.window.id) { + this.workspace.emitChangeActiveEditorView(); + } + } + }; + + get project() { + return this.editorView?.project; + } + + get innerProject() { + return this.editorView?.innerProject; + } + + get innerSkeleton() { + return this.editorView?.innerSkeleton; + } + + get innerSetters() { + return this.editorView?.innerSetters; + } + + get innerHotkey() { + return this.editorView?.innerHotkey; + } + + get editor() { + return this.editorView?.editor; + } + + get designer() { + return this.editorView?.designer; + } + + get plugins() { + return this.editorView?.plugins; + } + + get innerPlugins() { + return this.editorView?.innerPlugins; + } +} \ No newline at end of file diff --git a/packages/workspace/src/workspace.ts b/packages/workspace/src/workspace.ts new file mode 100644 index 0000000000..9f1abaa0fb --- /dev/null +++ b/packages/workspace/src/workspace.ts @@ -0,0 +1,379 @@ +import { IDesigner, ILowCodePluginManager, LowCodePluginManager } from '@alilc/lowcode-designer'; +import { createModuleEventBus, Editor, IEditor, IEventBus, makeObservable, obx } from '@alilc/lowcode-editor-core'; +import { IPublicApiPlugins, IPublicApiWorkspace, IPublicEnumPluginRegisterLevel, IPublicResourceList, IPublicTypeDisposable, IPublicTypeResourceType, IShellModelFactory } from '@alilc/lowcode-types'; +import { BasicContext } from './context/base-context'; +import { EditorWindow, WINDOW_STATE } from './window'; +import type { IEditorWindow } from './window'; +import { IResource, Resource } from './resource'; +import { IResourceType, ResourceType } from './resource-type'; +import { ISkeleton } from '@alilc/lowcode-editor-skeleton'; + +enum EVENT { + CHANGE_WINDOW = 'change_window', + + CHANGE_ACTIVE_WINDOW = 'change_active_window', + + WINDOW_RENDER_READY = 'window_render_ready', + + CHANGE_ACTIVE_EDITOR_VIEW = 'change_active_editor_view', +} + +const CHANGE_EVENT = 'resource.list.change'; + +export interface IWorkspace extends Omit<IPublicApiWorkspace< + LowCodePluginManager, + IEditorWindow +>, 'resourceList' | 'plugins' | 'openEditorWindow' | 'removeEditorWindow'> { + readonly registryInnerPlugin: (designer: IDesigner, editor: Editor, plugins: IPublicApiPlugins) => Promise<IPublicTypeDisposable>; + + readonly shellModelFactory: IShellModelFactory; + + enableAutoOpenFirstWindow: boolean; + + window: IEditorWindow; + + plugins: ILowCodePluginManager; + + skeleton: ISkeleton; + + resourceTypeMap: Map<string, ResourceType>; + + getResourceList(): IResource[]; + + getResourceType(resourceName: string): IResourceType; + + checkWindowQueue(): void; + + emitWindowRendererReady(): void; + + initWindow(): void; + + setActive(active: boolean): void; + + onChangeActiveEditorView(fn: () => void): IPublicTypeDisposable; + + emitChangeActiveEditorView(): void; + + openEditorWindowByResource(resource: IResource, sleep: boolean): Promise<void>; + + /** + * @deprecated + */ + removeEditorWindow(resourceName: string, id: string): void; + + removeEditorWindowByResource(resource: IResource): void; + + /** + * @deprecated + */ + openEditorWindow(name: string, title: string, options: Object, viewName?: string, sleep?: boolean): Promise<void>; +} + +export class Workspace implements IWorkspace { + context: BasicContext; + + enableAutoOpenFirstWindow: boolean; + + resourceTypeMap: Map<string, ResourceType> = new Map(); + + private emitter: IEventBus = createModuleEventBus('workspace'); + + private _isActive = false; + + private resourceList: IResource[] = []; + + get skeleton() { + return this.context.innerSkeleton; + } + + get plugins() { + return this.context.innerPlugins; + } + + get isActive() { + return this._isActive; + } + + get defaultResourceType(): ResourceType | null { + if (this.resourceTypeMap.size >= 1) { + return Array.from(this.resourceTypeMap.values())[0]; + } + + return null; + } + + @obx.ref windows: IEditorWindow[] = []; + + editorWindowMap: Map<string, IEditorWindow> = new Map<string, IEditorWindow>(); + + @obx.ref window: IEditorWindow; + + windowQueue: ({ + name: string; + title: string; + options: Object; + viewName?: string; + } | IResource)[] = []; + + constructor( + readonly registryInnerPlugin: (designer: IDesigner, editor: IEditor, plugins: IPublicApiPlugins) => Promise<IPublicTypeDisposable>, + readonly shellModelFactory: any, + ) { + this.context = new BasicContext(this, '', IPublicEnumPluginRegisterLevel.Workspace); + this.context.innerHotkey.activate(true); + makeObservable(this); + } + + checkWindowQueue() { + if (!this.windowQueue || !this.windowQueue.length) { + return; + } + + const windowInfo = this.windowQueue.shift(); + if (windowInfo instanceof Resource) { + this.openEditorWindowByResource(windowInfo); + } else if (windowInfo) { + this.openEditorWindow(windowInfo.name, windowInfo.title, windowInfo.options, windowInfo.viewName); + } + } + + async initWindow() { + if (!this.defaultResourceType || this.enableAutoOpenFirstWindow === false) { + return; + } + const resourceName = this.defaultResourceType.name; + const resource = new Resource({ + resourceName, + options: {}, + }, this.defaultResourceType, this); + this.window = new EditorWindow(resource, this, { + title: resource.title, + }); + await this.window.init(); + this.editorWindowMap.set(this.window.id, this.window); + this.windows = [...this.windows, this.window]; + this.emitChangeWindow(); + this.emitChangeActiveWindow(); + } + + setActive(value: boolean) { + this._isActive = value; + } + + async registerResourceType(resourceTypeModel: IPublicTypeResourceType): Promise<void> { + const resourceType = new ResourceType(resourceTypeModel); + this.resourceTypeMap.set(resourceTypeModel.resourceName, resourceType); + + if (!this.window && this.defaultResourceType && this._isActive) { + this.initWindow(); + } + } + + getResourceList() { + return this.resourceList; + } + + setResourceList(resourceList: IPublicResourceList) { + this.resourceList = resourceList.map(d => new Resource(d, this.getResourceType(d.resourceName), this)); + this.emitter.emit(CHANGE_EVENT, resourceList); + } + + onResourceListChange(fn: (resourceList: IPublicResourceList) => void): () => void { + this.emitter.on(CHANGE_EVENT, fn); + return () => { + this.emitter.off(CHANGE_EVENT, fn); + }; + } + + onWindowRendererReady(fn: () => void): IPublicTypeDisposable { + this.emitter.on(EVENT.WINDOW_RENDER_READY, fn); + return () => { + this.emitter.off(EVENT.WINDOW_RENDER_READY, fn); + }; + } + + emitWindowRendererReady() { + this.emitter.emit(EVENT.WINDOW_RENDER_READY); + } + + getResourceType(resourceName: string): IResourceType { + return this.resourceTypeMap.get(resourceName)!; + } + + removeResourceType(resourceName: string) { + if (this.resourceTypeMap.has(resourceName)) { + this.resourceTypeMap.delete(resourceName); + } + } + + removeEditorWindowById(id: string) { + const index = this.windows.findIndex(d => (d.id === id)); + this.remove(index); + } + + private async remove(index: number) { + if (index < 0) { + return; + } + const window = this.windows[index]; + this.windows.splice(index, 1); + this.window?.updateState(WINDOW_STATE.destroyed); + if (this.window === window) { + this.window = this.windows[index] || this.windows[index + 1] || this.windows[index - 1]; + if (this.window?.sleep) { + await this.window.init(); + } + this.emitChangeActiveWindow(); + } + this.emitChangeWindow(); + this.window?.updateState(WINDOW_STATE.active); + } + + removeEditorWindow(resourceName: string, id: string) { + const index = this.windows.findIndex(d => (d.resource?.name === resourceName && (d.title === id || d.resource.id === id))); + this.remove(index); + } + + removeEditorWindowByResource(resource: IResource) { + const index = this.windows.findIndex(d => (d.resource?.id === resource.id)); + this.remove(index); + } + + async openEditorWindowById(id: string) { + const window = this.editorWindowMap.get(id); + this.window?.updateState(WINDOW_STATE.inactive); + if (window) { + this.window = window; + if (window.sleep) { + await window.init(); + } + this.emitChangeActiveWindow(); + } + this.window?.updateState(WINDOW_STATE.active); + } + + async openEditorWindowByResource(resource: IResource, sleep: boolean = false): Promise<void> { + if (this.window && !this.window.sleep && !this.window?.initReady && !sleep) { + this.windowQueue.push(resource); + return; + } + + this.window?.updateState(WINDOW_STATE.inactive); + + const filterWindows = this.windows.filter(d => (d.resource?.id === resource.id)); + if (filterWindows && filterWindows.length) { + this.window = filterWindows[0]; + if (!sleep && this.window.sleep) { + await this.window.init(); + } else { + this.checkWindowQueue(); + } + this.emitChangeActiveWindow(); + this.window?.updateState(WINDOW_STATE.active); + return; + } + + const window = new EditorWindow(resource, this, { + title: resource.title, + options: resource.options, + viewName: resource.viewName, + sleep, + }); + + this.windows = [...this.windows, window]; + this.editorWindowMap.set(window.id, window); + if (sleep) { + this.emitChangeWindow(); + return; + } + this.window = window; + await this.window.init(); + this.emitChangeWindow(); + this.emitChangeActiveWindow(); + this.window?.updateState(WINDOW_STATE.active); + } + + async openEditorWindow(name: string, title: string, options: Object, viewName?: string, sleep?: boolean) { + if (this.window && !this.window.sleep && !this.window?.initReady && !sleep) { + this.windowQueue.push({ + name, title, options, viewName, + }); + return; + } + const resourceType = this.resourceTypeMap.get(name); + if (!resourceType) { + console.error(`${name} resourceType is not available`); + return; + } + this.window?.updateState(WINDOW_STATE.inactive); + const filterWindows = this.windows.filter(d => (d.resource?.name === name && d.resource.title == title) || (d.resource.id == title)); + if (filterWindows && filterWindows.length) { + this.window = filterWindows[0]; + if (!sleep && this.window.sleep) { + await this.window.init(); + } else { + this.checkWindowQueue(); + } + this.emitChangeActiveWindow(); + this.window?.updateState(WINDOW_STATE.active); + return; + } + const resource = new Resource({ + resourceName: name, + title, + options, + id: title?.toString(), + }, resourceType, this); + const window = new EditorWindow(resource, this, { + title, + options, + viewName, + sleep, + }); + this.windows = [...this.windows, window]; + this.editorWindowMap.set(window.id, window); + if (sleep) { + this.emitChangeWindow(); + return; + } + this.window = window; + await this.window.init(); + this.emitChangeWindow(); + this.emitChangeActiveWindow(); + this.window?.updateState(WINDOW_STATE.active); + } + + onChangeWindows(fn: () => void) { + this.emitter.on(EVENT.CHANGE_WINDOW, fn); + return () => { + this.emitter.removeListener(EVENT.CHANGE_WINDOW, fn); + }; + } + + onChangeActiveEditorView(fn: () => void) { + this.emitter.on(EVENT.CHANGE_ACTIVE_EDITOR_VIEW, fn); + return () => { + this.emitter.removeListener(EVENT.CHANGE_ACTIVE_EDITOR_VIEW, fn); + }; + } + + emitChangeActiveEditorView() { + this.emitter.emit(EVENT.CHANGE_ACTIVE_EDITOR_VIEW); + } + + emitChangeWindow() { + this.emitter.emit(EVENT.CHANGE_WINDOW); + } + + emitChangeActiveWindow() { + this.emitter.emit(EVENT.CHANGE_ACTIVE_WINDOW); + this.emitChangeActiveEditorView(); + } + + onChangeActiveWindow(fn: () => void) { + this.emitter.on(EVENT.CHANGE_ACTIVE_WINDOW, fn); + return () => { + this.emitter.removeListener(EVENT.CHANGE_ACTIVE_WINDOW, fn); + }; + } +} diff --git a/packages/workspace/tsconfig.json b/packages/workspace/tsconfig.json new file mode 100644 index 0000000000..c37b76ecc6 --- /dev/null +++ b/packages/workspace/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "./src/" + ] +} diff --git a/scripts/build.sh b/scripts/build.sh index bb203fa053..751e9094fe 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +set -e + lerna run build \ --scope @alilc/lowcode-types \ --scope @alilc/lowcode-utils \ @@ -8,24 +10,20 @@ 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-rax-renderer \ - --scope @alilc/lowcode-rax-simulator-renderer \ --scope @alilc/lowcode-react-renderer \ --scope @alilc/lowcode-react-simulator-renderer \ --scope @alilc/lowcode-renderer-core \ + --scope @alilc/lowcode-workspace \ --scope @alilc/lowcode-engine \ --stream lerna run build:umd \ --scope @alilc/lowcode-engine \ - --scope @alilc/lowcode-rax-simulator-renderer \ --scope @alilc/lowcode-react-simulator-renderer \ --scope @alilc/lowcode-react-renderer \ --stream cp ./packages/react-simulator-renderer/dist/js/* ./packages/engine/dist/js/ cp ./packages/react-simulator-renderer/dist/css/* ./packages/engine/dist/css/ - -cp ./packages/rax-simulator-renderer/dist/js/* ./packages/engine/dist/js/ -cp ./packages/rax-simulator-renderer/dist/css/* ./packages/engine/dist/css/ \ No newline at end of file diff --git a/scripts/set-repo.js b/scripts/set-repo.js new file mode 100644 index 0000000000..9bae66d053 --- /dev/null +++ b/scripts/set-repo.js @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +const path = require('path'); +const fs = require('fs-extra'); + +(async () => { + const root = path.join(__dirname, '../'); + const workspaces = ['modules', 'packages']; + for (const workspace of workspaces) { + const pkgDir = path.join(root, workspace); + const pkgs = await fs.readdir(pkgDir); + for (const pkg of pkgs) { + if (pkg.charAt(0) === '.') continue; + if (!(await fs.statSync(path.join(pkgDir, pkg))).isDirectory()) continue; + await setRepo({ + workspace, + pkgDir, + pkg, + }); + } + } + + async function setRepo(opts) { + const pkgDir = path.join(opts.pkgDir, opts.pkg); + const pkgPkgJSONPath = path.join(pkgDir, 'package.json'); + if (!fs.existsSync(pkgPkgJSONPath)) { + console.log(`${opts.pkg} exists`); + } else { + const pkgPkgJSON = require(pkgPkgJSONPath); + fs.writeJSONSync( + pkgPkgJSONPath, + Object.assign(pkgPkgJSON, { + repository: { + type: 'http', + url: `https://github.com/alibaba/lowcode-engine/tree/main/${opts.workspace}/${opts.pkg}`, + }, + bugs: 'https://github.com/alibaba/lowcode-engine/issues', + homepage: 'https://github.com/alibaba/lowcode-engine/#readme', + }), + { spaces: ' ' }, + ); + console.log(`[Write] ${opts.pkg}`); + } + } +})(); diff --git a/scripts/setup-skip-build.sh b/scripts/setup-skip-build.sh index d51c33c417..7c0ff6a273 100755 --- a/scripts/setup-skip-build.sh +++ b/scripts/setup-skip-build.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash rm -rf node_modules package-lock.json yarn.lock + +npm i lerna@4.0.0 + lerna clean -y find ./packages -type f -name "package-lock.json" -exec rm -f {} \; diff --git a/scripts/sync-oss.js b/scripts/sync-oss.js new file mode 100644 index 0000000000..2108e676d2 --- /dev/null +++ b/scripts/sync-oss.js @@ -0,0 +1,47 @@ +#!/usr/bin/env node +const http = require('http'); +const package = require('../packages/engine/package.json'); +const { version, name } = package; +const options = { + method: 'PUT', + hostname: 'uipaas-node.alibaba-inc.com', + path: '/staticAssets/cdn/packages', + headers: { + 'Content-Type': 'application/json', + Cookie: 'locale=en-us', + }, + maxRedirects: 20, +}; + +const onResponse = function (res) { + const chunks = []; + res.on('data', (chunk) => { + chunks.push(chunk); + }); + + res.on('end', () => { + const body = Buffer.concat(chunks); + console.table(JSON.stringify(JSON.parse(body.toString()), null, 2)); + }); + + res.on('error', (error) => { + console.error(error); + }); +}; + +const req = http.request(options, onResponse); + +const postData = JSON.stringify({ + packages: [ + { + packageName: name, + version, + }, + ], + // 可以发布指定源的 npm 包,默认公网 npm + useTnpm: true, +}); + +req.write(postData); + +req.end(); diff --git a/scripts/sync.sh b/scripts/sync.sh index 39a1f6e26a..3edac03845 100755 --- a/scripts/sync.sh +++ b/scripts/sync.sh @@ -10,8 +10,8 @@ tnpm sync @alilc/lowcode-designer tnpm sync @alilc/lowcode-plugin-designer tnpm sync @alilc/lowcode-plugin-outline-pane tnpm sync @alilc/lowcode-renderer-core -tnpm sync @alilc/lowcode-rax-renderer -tnpm sync @alilc/lowcode-rax-simulator-renderer tnpm sync @alilc/lowcode-react-renderer tnpm sync @alilc/lowcode-react-simulator-renderer -tnpm sync @alilc/lowcode-engine \ No newline at end of file +tnpm sync @alilc/lowcode-engine +tnpm sync @alilc/lowcode-workspace +tnpm sync @alilc/lowcode-plugin-command \ No newline at end of file diff --git a/specs/lowcode-spec.md b/specs/lowcode-spec.md deleted file mode 100644 index 65e05130d3..0000000000 --- a/specs/lowcode-spec.md +++ /dev/null @@ -1,1462 +0,0 @@ -# 《低代码引擎搭建协议规范》 - -# 1 介绍 - -## 1.1 本协议规范涉及的问题域 - -- 定义本协议版本号规范 -- 定义本协议中每个子规范需要被支持的 Level -- 定义本协议相关的领域名词 -- 定义搭建基础协议版本号规范(A) -- 定义搭建基础协议组件映射关系规范(A) -- 定义搭建基础协议组件树描述规范(A) -- 定义搭建基础协议国际化多语言支持规范(AA) -- 定义搭建基础协议无障碍访问规范(AAA) - - -## 1.2 协议草案起草人 - -- 撰写:月飞、康为、林熠 -- 审阅:大果、潕量、九神、元彦、戊子、屹凡、金禅、前道、天晟、戊子、游鹿、光弘、力皓 - - -## 1.3 版本号 - -1.0.0 - -## 1.4 协议版本号规范(A) - -本协议采用语义版本号,版本号格式为 `major.minor.patch` 的形式。 - -- major 是大版本号:用于发布不向下兼容的协议格式修改 -- minor 是小版本号:用于发布向下兼容的协议功能新增 -- patch 是补丁号:用于发布向下兼容的协议问题修正 - - -## 1.5 协议中子规范 Level 定义 - -| 规范等级 | 实现要求 | -| -------- | ---------------------------------------------------------------------------------- | -| A | 强制规范,必须实现;违反此类规范的协议描述数据将无法写入物料中心,不支持流通。 | -| AA | 推荐规范,推荐实现;遵守此类规范有助于业务未来的扩展性和跨团队合作研发效率的提升。 | -| AAA | 参考规范,根据业务场景实际诉求实现;是集团层面鼓励的技术实现引导。 | - - -## 1.6 名词术语 - -### 1.6.1 物料系统名词 - -- **基础组件(Basic Component)**:前端领域通用的基础组件,阿里巴巴前端委员会官方指定的基础组件库是 Fusion Next/AntD。 -- **图表组件(Chart Component)**:前端领域通用的图表组件,有代表性的图表组件库有 BizCharts。 -- **业务组件(Business Component)**:业务领域内基于基础组件之上定义的组件,可能会包含特定业务域的交互或者是业务数据,对外仅暴露可配置的属性,且必须发布到公域(如阿里 NPM);在同一个业务域内可以流通,但不需要确保可以跨业务域复用。 - - **低代码业务组件(Low-Code Business Component)**:通过低代码编辑器搭建而来,有别于源码开发的业务组件,属于业务组件中的一种类型,遵循业务组件的定义;同时低代码业务组件还可以通过低代码编辑器继续多次编辑。 -- **布局组件(Layout Component)**:前端领域通用的用于实现基础组件、图表组件、业务组件之间各类布局关系的组件,如三栏布局组件。 -- **区块(Block)**:通过低代码搭建的方式,将一系列业务组件、布局组件进行嵌套组合而成,不对外提供可配置的属性。可通过 区块容器组的包裹,实现区块内部具备有完整的样式、事件、生命周期管理、状态管理、数据流转机制。能独立存在和运行,可通过复制 schema 实现跨页面、跨应用的快速复用,保障功能和数据的正常。 -- **页面(Page)**:由组件 + 区块组合而成。由页面容器组件包裹,可描述页面级的状态管理和公共函数。 -- **模板(Template)**:特定垂直业务领域内的业务组件、区块可组合为单个页面,或者是再配合路由组合为多个页面集,统称为模板。 - - -### 1.6.2 低代码搭建系统名词 - -- **搭建编辑器**:使用可视化的方式实现页面搭建,支持组件 UI 编排、属性编辑、事件绑定、数据绑定,最终产出符合搭建基础协议规范的数据。 - - **属性面板**:低代码编辑器内部用于组件、区块、页面的属性编辑、事件绑定、数据绑定的操作面板。 - - **画布面板**:低代码编辑器内部用于 UI 编排的操作面板。 - - **大纲面板**:低代码编辑器内部用于页面组件树展示的面板。 -- **编辑器框架**:搭建编辑器的基础框架,包含主题配置机制、插件机制、setter 控件机制、快捷键管理、扩展点管理等底层基础设施。 -- **入料模块**:专注于物料接入,能自动扫描、解析源码组件,并最终产出一份符合《低代码引擎物料协议规范》的 Schema JSON。 -- **编排模块**:专注于 Schema 可视化编排,以可视化的交互方式提供页面结构编排服务,并最终产出一份符合《低代码搭建基础协议规范》的 Schema JSON。 -- **渲染模块**:专注于将 Schema JSON 渲染为 UI 界面,最终呈现一个可交互的页面。 -- **出码模块 Schema2Code**:专注于通过 Schema JSON 生成高质量源代码,将符合《低代码搭建基础协议规范》的 Schema JSON 数据分别转化为面向 React / Rax / 阿里小程序等终端可渲染的代码。 -- **事件绑定**:是指为某个组件的某个事件绑定相关的事件处理动作,比如为某个组件的**点击事件**绑定**一段处理函数**或**响应动作**(比如弹出对话框),每个组件可绑定的事件由该组件自行定义。 -- **数据绑定**:是指为某个组件的某个属性绑定用于该属性使用的数据。 -- **生命周期**: 一般指某个对象的生老病死,本文中指某个实体(组件、容器、区块等等)的创建、加载、显示、销毁等关键生命阶段的统称。 - -## 1.7 背景 - -- **协议目标**: 通过约束低代码引擎的搭建协议规范,让上层低代码编辑器的产出物(低代码业务组件、区块、应用)保持一致性,可跨低代码研发平台进行流通而提效,亦不阻碍集团业务间融合的发展。  -- **协议通**: - - 协议顶层结构统一 - - 协议 schema 具备有完整的描述能力,包含版本、国际化、组件树、组件映射关系等; - - 顶层属性 key、value 值的格式,必须保持一致; - - 组件树描述统一 - - 源码组件描述; - - 页面、区块、低代码业务组件这三种容器组件的描述; - - 数据流描述,包含数据请求、数据状态管理、数据绑定描述; - - 事件描述,包含统一事件上下文、统一搭建 API; -- **物料通**:指在相同领域内的不同搭建产品,可直接使用的物料。比如模版、区块、组件; - -## 1.8 受众 - -本协议适用于所有使用低代码搭建平台来开发页面或组件的开发者,以及围绕此协议的相关工具或工程化方案的开发者。阅读及使用本协议,需要对低代码搭建平台的交互和实现有一定的了解,对前端开发相关技术栈的熟悉也会有帮助,协议中对通用的前端相关术语不会做进一步的解释说明。 - -## 1.9 使用范围 - -本协议描述的是低代码搭建平台产物(应用、页面、区块、组件)的 schema 结构,以及实现其数据状态更新(内置 api)、能力扩展、国际化等方面完整,只在低代码搭建场景下可用; - -## 1.10 协议目标 - -一套面向开发者的 schema 规范,用于规范化约束搭建编辑器的输出,以及渲染模块和出码模块的输入,将搭建编辑器、渲染模块、出码模块解耦,保障搭建编辑器、渲染模块、出码模块的独立升级。 - -## 1.11 设计说明 - -- **语义化**:语义清晰,简明易懂,可读性强。 -- **渐进性描述**:搭建的本质是通过 源码组件 进行嵌套组合,从小往大、依次组合生成 组件、区块、页面,最终通过云端构建生成 应用 的过程。因此在搭建基础协议中,我们需要知道如何去渐进性的描述组件、区块、页面、应用这 4 个实体概念。 -- **生成标准源码**:明确每一个属性与源码对应的转换关系,可生成跟手写无差异的高质量标准源代码。 -- **可流通性**:产物能在不同搭建产品中流通,不涉及任何私域数据存储。 -- **面向多端**:不能仅面向 React,还有小程序等多端。 -- **支持国际化&无障碍访问标准的实现** - - -# 2 协议结构 - -协议最顶层结构如下,包含5方面的描述内容: - -- version { String } 当前协议版本号 -- componentsMap { Array } 组件映射关系 -- componentsTree { Array } 描述模版/页面/区块/低代码业务组件的组件树 -- utils { Array } 工具类扩展映射关系 -- i18n { Object } 国际化语料 - - -描述举例: - -```json -{ - "version": "1.0.0", // 当前协议版本号 - "componentsMap": [{ // 组件描述 - "componentName": "Button", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Select", - "subName": "Button" - }], - "utils": [{ - "name": "clone", - "type": "npm", - "content": { - "package": "lodash", - "version": "0.0.1", - "exportName": "clone", - "subName": "", - "destructuring": false, - "main": "/lib/clone" - } - }, { - "name": "moment", - "type": "npm", - "content": { - "package": "@alifd/next", - "version": "0.0.1", - "exportName": "Moment", - "subName": "", - "destructuring": true, - "main": "" - } - }], - "componentsTree": [{ // 描述内容,值类型 Array - "componentName": "Page", // 单个页面,枚举类型 Page|Block|Component - "fileName": "Page1", - "props": {}, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "children": [{ - "componentName": "Div", - "props": { - "className": "" - }, - "children": [{ - "componentName": "Button", - "props": { - "prop1": 1234, // 简单 json 数据 - "prop2": [{ // 简单 json 数据 - "label": "选项1", - "value": 1 - }, { - "label": "选项2", - "value": 2 - }], - "prop3": [{ - "name": "myName", - "rule": { - "type": "JSExpression", - "value": "/\w+/i" - } - }], - "valueBind": { // 变量绑定 - "type": "JSExpression", - "value": "this.state.user.name" - }, - "onClick": { // 动作绑定 - "type": "JSFunction", - "value": "function(e) { console.log(e.target.innerText) }" - }, - "onClick2": { // 动作绑定 2 - "type": "JSExpression", - "value": "this.submit" - } - } - }] - }] - }], - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China" - } - } -} -``` - -## 2.1 协议版本号(A) - -定义当前协议 schema 的版本号,不同的版本号对应不同的渲染 SDK,以保障不同版本搭建协议产物的正常渲染; - - -| 根属性名称 | 类型 | 说明 | 变量支持 | 默认值 | -| ---------- | ------ | ---------- | -------- | ------ | -| version | String | 协议版本号 | - | 1.0.0 | - - -描述示例: - -```javascript -{ - "version": "1.0.0" -} -``` - -## 2.2 组件映射关系(A) - -协议中用于描述 componentName 到公域组件映射关系的规范。 - - -| 参数 | 说明 | 类型 | 变量支持 | 默认值 | -| --------------- | ---------------------- | ------------------------- | -------- | ------ | -| componentsMap[] | 描述组件映射关系的集合 | Array\<**ComponentMap**\> | - | null | - -**ComponentMap 结构描述**如下: - -| 参数 | 说明 | 类型 | 变量支持 | 默认值 | -| ------------- | ------------------------------------------------------------------------------------------------------ | ------- | -------- | ------ | -| componentName | 协议中的组件名,唯一性,对应包导出的组件名,是一个有效的 **JS 标识符**,而且是大写字母打头 | String | - | - | -| package | npm 公域的 package name | String | - | - | -| version | package version | String | - | - | -| destructuring | 使用解构方式对模块进行导出 | Boolean | - | - | -| exportName | 包导出的组件名 | String | - | - | -| subName | 下标子组件名称 | String | - | | -| main | 包导出组件入口文件路径 | String | - | - | - - -描述示例: - -```json -{ - "componentsMap": [{ - "componentName": "Button", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true - }, { - "componentName": "MySelect", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Select" - }, { - "componentName": "ButtonGroup", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Button", - "subName": "Group" - }, { - "componentName": "RadioGroup", - "package": "@alifd/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Radio", - "subName": "Group" - }, { - "componentName": "CustomCard", - "package": "@ali/custom-card", - "version": "1.0.0" - }, { - "componentName": "CustomInput", - "package": "@ali/custom", - "version": "1.0.0", - "main": "/lib/input", - "destructuring": true, - "exportName": "Input" - }] -} -``` - -出码结果: - -```javascript -// 使用解构方式, destructuring is true. -import { Button } from '@alifd/next'; - -// 使用解构方式,且 exportName 和 componentName 不同 -import { Select as MySelect } from '@alifd/next'; - -// 使用解构方式,并导出其子组件 -import { Button } from '@alifd/next'; -const ButtonGroup = Button.Group; - -import { Radio } from '@alifd/next'; -const RadioGroup = Radio.Group; - -// 不使用解构方式进行导出 -import CustomCard from '@ali/custom-card'; - -// 使用特定路径进行导出 -import { Input as CustomInput } from '@ali/custom/lib/input'; - -``` - - -## 2.3 组件树描述(A) - - -协议中用于描述搭建出来的组件树结构的规范,整个组件树的描述由**组件结构**&**容器结构**两种结构嵌套构成。 - -- 组件结构:描述单个组件的名称、属性、子集的结构; -- 容器结构:描述单个容器的数据、自定义方法、生命周期的结构,用于将完整页面进行模块化拆分。 - -与源码对应的转换关系如下: - -- 组件结构:转换成一个 .jsx 文件内 React Class 类 render 函数返回的 **jsx** 代码。 -- 容器结构:将转换成一个标准文件,如 React 的 jsx 文件, export 一个 React Class,包含生命周期定义、自定义方法、事件属性绑定、异步数据请求等。 - -### 2.3.1 基础结构描述 (A) - -此部分定义了组件结构、容器结构的公共基础字段。 - -> 阅读时可先跳到后续章节,待需要时回来参考阅读 - -#### 2.3.1.1 Props 结构描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ----------- | ------------ | ------ | -------- | ------ | ------------------------------------- | -| id | 组件 ID | String | ✅ | - | 系统属性 | -| className | 组件样式类名 | String | ✅ | - | 系统属性,支持变量表达式 | -| style | 组件内联样式 | Object | ✅ | - | 系统属性,单个内联样式属性值 | -| ref | 组件 ref 名称 | String | ✅ | - | 可通过 `this.$(ref)` 获取组件实例 | -| extendProps | 组件继承属性 | 变量 | ✅ | - | 仅支持变量绑定,常用于继承属性对象 | -| ... | 组件私有属性 | - | - | - | | - -#### 2.3.1.2 css/less/scss 样式描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | -| ------------- | -------------------------------------------------------------------------- | ------ | -------- | ------ | -| css/less/scss | 用于描述容器组件内部节点的样式,对应生成一个独立的样式文件,不支持 @import | String | - | null | - -描述示例: - -```json -{ - "css": "body {font-size: 12px;} .table { width: 100px; }" -} -``` - -#### 2.3.1.3 ComponentDataSource 对象描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ----------- | ---------------------- | -------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | -| list[] | 数据源列表 | Array\<**ComponentDataSourceItem**\> | - | - | 成为为单个请求配置, 内容定义详见 [ComponentDataSourceItem 对象描述](#2314-componentdatasourceitem-对象描述) | -| dataHandler | 所有请求数据的处理函数 | Function | - | - | 详见 [dataHandler Function 描述](#2317-datahandler-function 描述) | - -#### 2.3.1.4 ComponentDataSourceItem 对象描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| -------------- | ---------------------------- | ---------------------------------------------------- | -------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| id | 数据请求 ID 标识 | String | - | - | | -| isInit | 是否为初始数据 | Boolean | ✅ | true | 值为 true 时,将在组件初始化渲染时自动发送当前数据请求 | -| isSync | 是否需要串行执行 | Boolean | ✅ | false | 值为 true 时,当前请求将被串行执行 | -| type | 数据请求类型 | String | - | fetch | 支持四种类型:fetch/mtop/jsonp/custom | -| shouldFetch | 本次请求是否可以正常请求 | (options: ComponentDataSourceItemOptions) => boolean | - | ```() => true``` | function 参数参考 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | -| willFetch | 单个数据结果请求参数处理函数 | Function | - | options => options | 只接受一个参数(options),返回值作为请求的 options,当处理异常时,使用原 options。也可以返回一个 Promise,resolve 的值作为请求的 options,reject 时,使用原 options | -| requestHandler | 自定义扩展的外部请求处理器 | Function | - | - | 仅 type='custom' 时生效 | -| dataHandler | request 成功后的回调函数 | Function | - | `response => response.data` | 参数: 请求成功后 promise 的 value 值 | -| errorHandler | request 失败后的回调函数 | Function | - | - | 参数: 请求出错 promise 的 error 内容 | -| options {} | 请求参数 | **ComponentDataSourceItemOptions** | - | - | 每种请求类型对应不同参数, 详见 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述) | - -**关于 dataHandler 于 errorHandler 的细节说明:** - -request 返回的是一个 promise,dataHandler 和 errorHandler 遵循 Promise 对象的 then 方法,实际使用方式如下: - -```ts -// 伪代码 -try { - const result = await request(fetchConfig).then(dataHandler, errorHandler); - dataSourceItem.data = result; - dataSourceItem.status = 'success'; -} catch (err) { - dataSourceItem.error = err; - dataSourceItem.status = 'error'; -} -``` -**注意:** -- dataHandler 和 errorHandler 只会走其中的一个回调 -- 它们都有修改 promise 状态的机会,意味着可以修改当前数据源最终状态 -- 最后返回的结果会被认为是当前数据源的最终结果,如果被 catch 了,那么会认为数据源请求出错 -- dataHandler 会有默认值,考虑到返回结果入参都是 response 完整对象,默认值会返回 `response.data`,errorHandler 没有默认值 - - -#### 2.3.1.5 ComponentDataSourceItemOptions 对象描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------- | ------------ | ------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------- | -| uri | 请求地址 | String | ✅ | - | | -| params | 请求参数 | Object | ✅ | {} | 当前数据源默认请求参数(在运行时会被实际的 load 方法的参数替换,如果 load 的 params 没有则会使用当前 params) | -| method | 请求方法 | String | ✅ | GET | | -| isCors | 是否支持跨域 | Boolean | ✅ | true | 对应 `credentials = 'include'` | -| timeout | 超时时长 | Number | ✅ | 5000 | 单位 ms | -| headers | 请求头信息 | Object | ✅ | - | 自定义请求头 | - - - -#### 2.3.1.6 ComponentLifeCycles 对象描述 - -生命周期对象,schema 面向多端,不同 DSL 有不同的生命周期方法: - -- React:对于中后台 PC 物料,已明确使用 React 作为最终渲染框架,因此提案采用 [React16 标准生命周期方法](https://reactjs.org/docs/react-component.html)标准来定义生命周期方法,降低理解成本,支持生命周期如下: - - constructor(props, context)  - - 说明:初始化渲染时执行,常用于设置 state 值。 - - render()  - - 说明:执行于容器组件 React Class 的 render 方法最前,常用于计算变量挂载到 this 对象上,供 props 上属性绑定。此 render() 方法不需要设置 return 返回值。 - - componentDidMount() - - 说明:组件已加载 - - componentDidUpdate(prevProps, prevState, snapshot) - - 说明:组件已更新 - - componentWillUnmount() - - 说明:组件即将从 DOM 中移除 - - componentDidCatch(error, info) - - 说明:组件捕获到异常 -- Rax:目前没有使用生命周期,使用 hooks 替代生命周期; - -该对象由一系列 key-value 组成,key 为生命周期方法名,value 为 JSFunction 的描述,详见下方示例: - -```json -{ - "componentDidMount": { // key 为上文中 React 的生命周期方法名 - "type": "JSFunction", // type 目前仅支持 JSFunction - "value": "function() {\ // value 为 javascript 函数 - console.log('did mount');\ - }" - }, - "componentWillUnmount": { - "type": "JSFunction", - "value": "function() {\ - console.log('will unmount');\ - }" - } - ... -}, -``` - - -#### 2.3.1.7 dataHandler Function 描述 - -- 参数:为 dataMap 对象,包含字段如下: - - key: 数据 id - - value: 单个请求结果 -- 返回值:数据对象 data,将会在渲染引擎和 schemaToCode 中通过调用 `this.setState(...)` 将返回的数据对象生效到 state 中;支持返回一个 Promise,通过 `resolve(返回数据)`,常用于串行发送请求场景。 - -#### 2.3.1.8 ComponentPropDefinition 对象描述 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------------ | ---------- | -------------- | -------- | --------- | ----------------------------------------------------------------------------------------------------------------- | -| name | 属性名称 | String | - | - | | -| propType | 属性类型 | String\|Object | - | - | 具体值内容结构,参考《低代码引擎物料协议规范》 内的 “2.2.2.3 组件属性信息”中描述的**基本类型**和**复合类型** | -| description | 属性描述 | String | - | '' | | -| defaultValue | 属性默认值 | Any | - | undefined | 当 defaultValue 和 defaultProps 中存在同一个 prop 的默认值时,优先使用 defaultValue。 | - -范例: -```json -{ - "propDefinitions": [{ - "name": "title", - "propType": "string", - "defaultValue": "Default Title" - }, { - "name": "onClick", - "propType": "func" - }] - ... -}, -``` - -### 2.3.2 组件结构描述(A) - -对应生成源码开发体系中 render 函数返回的 jsx 代码,主要描述有以下属性: - - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| ------------- | ---------------------- | ---------------- | -------- | ----------------- | ---------------------------------------------------------------------------------------------------------- | -| id | 组件唯一标识 | String | - | | 可选, 组件 id 由引擎随机生成(UUID),并保证唯一性,消费方为上层应用平台,在组件发生移动等场景需保持 id 不变 | -| componentName | 组件名称 | String | - | Div | 必填,首字母大写, 同 [componentsMap](#22-组件映射关系 a) 中的要求 | -| props {} | 组件属性对象 | **Props** | - | {} | 必填, 详见 [Props 结构描述](#2311-props-结构描述) | -| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | -| loop | 循环数据 | Array | ✅ | - | 选填,默认不进行循环渲染;支持变量表达式 | -| loopArgs | 循环迭代对象、索引名称 | [String, String] | | ["item", "index"] | 选填,仅支持字符串 | -| children | 子组件 | Array | | | 选填,支持变量表达式 | - - -描述举例: - -```json -{ - "componentName": "Button", - "props": { - "className": "btn", - "style": { - "width": 100, - "height": 20 - }, - "text": "submit", - "onClick": { - "type": "JSFunction", - "value": "function(e) {\ - console.log('btn click')\ - }" - } - }, - "condition": { - "type": "JSExpression", - "value": "!!this.state.isshow" - }, - "loop": [], - "loopArgs": ["item", "index"], - "children": [] -} -``` - - -### 2.3.3 容器结构描述 (A)  - -容器是一类特殊的组件,在组件能力基础上增加了对生命周期对象、自定义方法、样式文件、数据源等信息的描述。包含**低代码业务组件容器 Component**、**区块容器 Block**、**页面容器 Page** 3 种。主要描述有以下属性: - -- 组件类型:componentName -- 文件名称:fileName -- 组件属性:props -- state 状态管理:state -- 生命周期 Hook 方法:lifeCycles -- 自定义方法设置:methods -- 异步数据源配置:dataSource -- 条件渲染:condition -- 样式文件:css/less/scss - - -详细描述: - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | 备注 | -| --------------- | -------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | ------ | ----------------------------------------------------------------------------------------------------------------------------- | -| componentName | 组件名称 | 枚举类型,包括`'Page'` (代表页面容器)、`'Block'` (代表区块容器)、`'Component'` (代表低代码业务组件容器) | - | 'Div' | 必填,首字母大写 | -| fileName | 文件名称 | String | - | - | 必填,英文 | -| props { } | 组件属性对象 | **Props** | - | {} | 必填,详见 [Props 结构描述](#2311-props-结构描述) | -| static | 低代码业务组件类的静态对象 | | | | | -| defaultProps | 低代码业务组件默认属性 | Object | - | - | 选填,仅用于定义低代码业务组件的默认属性 | -| propDefinitions | 低代码业务组件属性类型定义 | **Array\<ComponentPropDefinition\>** | - | - | 选填,仅用于定义低代码业务组件的属性数据类型。详见 [ComponentPropDefinition 对象描述](#2318-componentpropdefinition-对象描述) | -| condition | 渲染条件 | Boolean | ✅ | true | 选填,根据表达式结果判断是否渲染物料;支持变量表达式 | -| state | 容器初始数据 | Object | ✅ | - | 选填,支持变量表达式 | -| children | 子组件 | Array | - | | 选填,支持变量表达式 | -| css/less/scss | 样式属性 | String | ✅ | - | 选填, 详见 [css/less/scss 样式描述](#2312-csslessscss 样式描述) | -| lifeCycles | 生命周期对象 | **ComponentLifeCycles** | - | - | 详见 [ComponentLifeCycles 对象描述](#2316-componentlifecycles-对象描述) | -| methods | 自定义方法对象 | Object | - | - | 选填,对象成员为函数类型 | -| dataSource {} | 数据源对象 | **ComponentDataSource** | - | - | 选填,异步数据源, 详见 [ComponentDataSource 对象描述](#2313-componentdatasource-对象描述) | - - - -#### 完整描述示例 - -描述示例 1:(正常 fetch/mtop/jsonp 请求): - -```json -{ - "componentName": "Block", - "fileName": "block-1", - "props": { - "className": "luna-page", - "style": { - "background": "#dd2727" - } - }, - "children": [{ - "componentName": "Button", - "props": { - "text": { - "type": "JSExpression", - "value": "this.state.btnText" - } - } - }], - "state": { - "btnText": "submit" - }, - "css": "body {font-size: 12px;}", - "lifeCycles": { - "componentDidMount": { - "type": "JSFunction", - "value": "function() {\ - console.log('did mount');\ - }" - }, - "componentWillUnmount": { - "type": "JSFunction", - "value": "function() {\ - console.log('will unmount');\ - }" - } - }, - "methods": { - "testFunc": { - "type": "JSFunction", - "value": "function() {\ - console.log('test func');\ - }" - } - }, - "dataSource": { - "list": [{ - "id": "list", - "isInit": true, - "type": "fetch/mtop/jsonp", - "options": { - "uri": "", - "params": {}, - "method": "GET", - "isCors": true, - "timeout": 5000, - "headers": {} - }, - "dataHandler": { - "type": "JSFunction", - "value": "function(data, err) {}" - } - }], - "dataHandler": { - "type": "JSFunction", - "value": "function(dataMap) { }" - } - }, - "condition": { - "type": "JSExpression", - "value": "!!this.state.isShow" - } -} -``` - -描述示例 2:(自定义扩展请求处理器类型): - -```json -{ - "componentName": "Block", - "fileName": "block-1", - "props": { - "className": "luna-page", - "style": { - "background": "#dd2727" - } - }, - ... - "dataSource": { - "list": [{ - "id": "list", - "isInit": true, - "type": "custom", - "requestHandler": { - "type": "JSFunction", - "value": "this.utils.hsfHandler" - }, - "options": { - "uri": "hsf://xxx", - "param1": "a", - "param2": "b", - ... - }, - "dataHandler": { - "type": "JSFunction", - "value": "function(data, err) { }" - } - }], - "dataHandler": { - "type": "JSFunction", - "value": "function(dataMap) { }" - } - } -} -``` - -### 2.3.4 属性值类型描述(A) - -在上述**组件结构**和**容器结构**描述中,每一个属性所对应的值,除了传统的 JS 值类型(String、Number、Object、Array、Boolean)外,还包含有**节点类型**、**事件函数类型**、**变量类型**等多种复杂类型;接下来将对于复杂类型的详细描述方式进行详细介绍。 - -#### 2.3.4.1 节点类型(A) - -通常用于描述组件的某一个属性为 **ReactNode** 或 **Function-Return-ReactNode** 的场景。该类属性的描述均以 **JSSlot** 的方式进行描述,详细描述如下: - -**ReactNode** 描述: - -| 参数 | 说明 | 值类型 | 默认值 | 备注 | -| ----- | ---------- | --------------------- | -------- | -------------------------------------------------------------- | -| type | 值类型描述 | String | 'JSSlot' | 固定值 | -| value | 具体的值 | Array\<NodeSchema\> | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述(A)) | - - -举例描述:如 **Card** 的 **title** 属性 - -```json -{ - "componentName": "Card", - "props": { - "title": { - "type": "JSSlot", - "value": [{ - "componentName": "Icon", - "props": {} - },{ - "componentName": "Text", - "props": {} - }] - }, - ... - } -} - -``` - - -**Function-Return-ReactNode** 描述: - -| 参数 | 说明 | 值类型 | 默认值 | 备注 | -| ------ | ---------- | --------------------- | -------- | -------------------------------------------------------------- | -| type | 值类型描述 | String | 'JSSlot' | 固定值 | -| value | 具体的值 | Array\<NodeSchema\> | null | 内容为 NodeSchema 类型,详见[组件结构描述](#232-组件结构描述 a) | -| params | 函数的参数 | Array\<String\> | null | 函数的入参,其子节点可以通过 `this[参数名]` 来获取对应的参数。 | - - -举例描述:如 **Table.Column** 的 **cell** 属性 - -```json -{ - "componentName": "TabelColumn", - "props": { - "cell": { - "type": "JSSlot", - "params": ["value", "index", "record"], - "value": [{ - "componentName": "Input", - "props": {} - }] - }, - ... - } -} - -``` - -#### 2.4.3.2 事件函数类型(A) - -协议内的事件描述,主要包含**容器结构**的**生命周期**和**自定义方法**,以及**组件结构**的**事件函数类属性**三类。所有事件函数的描述,均以 **JSFunction** 的方式进行描述,保留与原组件属性、生命周期(React / 小程序)一致的输入参数,并给所有事件函数 binding 统一一致的上下文(当前组件所在容器结构的 **this** 对象)。 - -**事件函数类型**的属性值描述如下: - -```json -{ - "type": "JSFunction", - "value": "function onClick(){\ - console.log(123);\ - }" -} -``` - -描述举例: - -```json -{ - "componentName": "Block", - "fileName": "block1", - "props": {}, - "state": { - "name": "lucy" - }, - "lifeCycles": { - "componentDidMount": { - "type": "JSFunction", - "value": "function() {\ - console.log('did mount');\ - }" - }, - "componentWillUnmount": { - "type": "JSFunction", - "value": "function() {\ - console.log('will unmount');\ - }" - } - }, - "methods": { - "getNum": { - "type": "JSFunction", - "value": "function() {\ - console.log('名称是:' + this.state.name)\ - }" - } - }, - "children": [{ - "componentName": "Button", - "props": { - "text": "按钮", - "onClick": { - "type": "JSFunction", - "value": "function(e) {\ - console.log(e.target.innerText);\ - }" - } - } - }] -} -``` - -#### 2.4.3.3 变量类型(A) - -在上述**组件结构** 或**容器结构**中,有多个属性的值类型是支持变量类型的,通常会通过变量形式来绑定某个数据,所有的变量表达式均通过 JSExpression 表达式,上下文与事件函数描述一致,表达式内通过 **this** 对象获取上下文; - -变量**类型**的属性值描述如下: - - -- return 数字类型 - - ```json - { - "type": "JSExpression", - "value": "this.state.num" - } - ``` -- return 数字类型 - - ```json - { - "type": "JSExpression", - "value": "this.state.num - this.state.num2" - } - ``` -- return "8万" 字符串类型 - - ```json - { - "type": "JSExpression", - "value": "`${this.state.num}万`" - } - ``` -- return "8万" 字符串类型 - - ```json - { - "type": "JSExpression", - "value": "this.state.num + '万'" - } - ``` -- return 13 数字类型 - - ```json - { - "type": "JSExpression", - "value": "getNum(this.state.num, this.state.num2)" - } - ``` -- return true 布尔类型 - - ```json - { - "type": "JSExpression", - "value": "this.state.num > this.state.num2" - } - ``` - -描述举例: - -```json -{ - "componentName": "Block", - "fileName": "block1", - "props": {}, - "state": { - "num": 8, - "num2": 5 - }, - "methods": { - "getNum": { - "type": "JSFunction", - "value": "function(a, b){\ - return a + b;\ - }" - } - }, - "children": [{ - "componentName": "Button", - "props": { - "text": { - "type": "JSExpression", - "value": "getNum(this.state.num, this.state.num2) + '万'" - } - }, - "condition": { - "type": "JSExpression", - "value": "this.state.num > this.state.num2" - } - }] -} -``` - -#### 2.4.3.4 国际化多语言类型(AA) - -协议内的一些文本值内容,我们希望是和协议全局的国际化多语言语料是关联的,会按照全局国际化语言环境的不同使用对应的语料。所有国际化多语言值均以 **i18n** 结构描述。这样可以更为清晰且结构化得表达使用场景。 - -**国际化多语言类型**的属性值类型描述如下: - -```typescript -type Ti18n = { - type: 'i18n'; - key: string; // i18n 结构中字段的 key 标识符 - params?: Record<string, JSDataType | JSExpression>; // 模版型 i18n 文案的入参,JSDataType 指代传统 JS 值类型 -} -``` - -其中 `key` 对应协议 `i18n` 内容的语料键值,`params` 为语料为字符串模板时的变量内容。 - -假设协议已加入如下 i18n 内容: -```json -{ - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "${name}博士" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "Doctor ${name}" - } - } -} -``` - -**国际化多语言类型**简单范例: - -```json -{ - "type": "i18n", - "key": "i18n-jwg27yo4" -} -``` - -**国际化多语言类型**模板范例: - -```json -{ - "type": "i18n", - "key": "i18n-jwg27yo3", - "params": { - "name": "Strange" - } -} -``` - -描述举例: - -```json -{ - "componentName": "Button", - "props": { - "text": { - "type": "i18n", - "key": "i18n-jwg27yo4" - } - } -} -``` - - -### 2.3.5 上下文 API 描述(A) - -在上述**事件类型描述**和**变量类型描述**中,在函数或 JS 表达式内,均可以通过 **this** 对象获取当前组件所在容器(React Class)的实例化对象,在搭建场景下的渲染模块和出码模块实现上,统一约定了该实例化 **this** 对象下所挂载的最小 API 集合,以保障搭建协议具备有一致的**数据流**和**事件上下文**。  - -#### 2.3.5.1 容器 API: - -| 参数 | 说明 | 类型 | 备注 | -| ----------------------------------- | --------------------------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------- | -| **this {}** | 当前区块容器的实例对象 | Class Instance | - | -| *this*.state | 三种容器实例的数据对象 state | Object | - | -| *this*.setState(newState, callback) | 三种容器实例更新数据的方法 | Function | 这个 setState 通常会异步执行,详见下文 [setState](#setstate) | -| *this*.customMethod() | 三种容器实例的自定义方法 | Function | - | -| *this*.dataSourceMap {} | 三种容器实例的数据源对象 Map | Object | 单个请求的 id 为 key, value 详见下文 [DataSourceMapItem 结构描述](#datasourcemapitem-结构描述) | -| *this*.reloadDataSource() | 三种容器实例的初始化异步数据请求重载 | Function | 返回 \<Promise\> | -| **this.page {}** | 当前页面容器的实例对象 | Class Instance | | -| *this.page*.props | 读取页面路由,参数等相关信息 | Object | query 查询参数 { key: value } 形式;path 路径;uri 页面唯一标识;其它扩展字段 | -| *this.page*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.page` 中的其他 API | -| **this.component {}** | 当前低代码业务组件容器的实例对象 | Class Instance | | -| *this.component*.props | 读取低代码业务组件容器的外部传入的 props | Object | | -| *this.component*.xxx | 继承 this 对象所有 API | | 此处 `xxx` 代指 `this.component` 中的其他 API | -| **this.$(ref)** | 获取组件的引用(单个) | Component Instance | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;若有同名的,则会返回第一个匹配的。 | -| **this.$$(ref)** | 获取组件的引用(所有同名的) | Array of Component Instances | `ref` 对应组件上配置的 `ref` 属性,用于唯一标识一个组件;总是返回一个数组,里面是所有匹配 `ref` 的组件的引用。 | - -##### setState - -`setState()` 将对容器 `state` 的更改排入队列,并通知低代码引擎需要使用更新后的 `state` 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式。 - -请将 `setState()` 视为请求而不是立即更新组件的命令。为了更好的感知性能,低代码引擎会延迟调用它,然后通过一次传递更新多个组件。低代码引擎并不会保证 state 的变更会立即生效。 - -`setState()` 并不总是立即更新组件, 它会批量推迟更新。这使得在调用 `setState()` 后立即读取 `this.state` 成为了隐患。为了消除隐患,请使用 `setState` 的回调函数(`setState(updater, callback)`),`callback` 将在应用更新后触发。即,如下例所示: - -```js -this.setState(newState, () => { - // 在这里更新已经生效了 - // 可以通过 this.state 拿到更新后的状态 - console.log(this.state); -}); - -// ⚠注意:这里拿到的并不是更新后的状态,这里还是之前的状态 -console.log(this.state); -``` - -如需基于之前的 `state` 来设置当前的 `state`,则可以将传递一个 `updater` 函数:`(state, props) => newState`,例如: - -```js -this.setState((prevState) => ({ count: prevState.count + 1 })); -``` - -为了方便更新部分状态,`setState` 会将 `newState` 浅合并到新的 `state` 上。 - - -##### DataSourceMapItem 结构描述 - -| 参数 | 说明 | 类型 | 备注 | -| ------------ | -------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------ | -| load(params) | 调用单个数据源 | Function | 当前参数 params 会替换 [ComponentDataSourceItemOptions 对象描述](#2315-componentdatasourceitemoptions-对象描述)中的 params 内容 | -| status | 获取单个数据源上次请求状态 | String | loading、loaded、error、init | -| data | 获取上次请求成功后的数据 | Any | | -| error | 获取上次请求失败的错误对象 | Error 对象 | | - -备注: 如果组件没有在区块容器内,而是直接在页面内,那么 `this === this.page` - - -#### 2.3.5.2 循环数据 API - -获取在循环场景下的数据对象。举例:上层组件设置了 loop 循环数据,且设置了 `loopArgs:["item", "index"]`,当前组件的属性表达式或绑定的事件函数中,可以通过 this 上下文获取所在循环的数据环境;默认值为 `['item','index']` ,如有多层循环,需要自定义不同 loopArgs,同样通过 `this[自定义循环别名]` 获取对应的循环数据和序号; - - -| 参数 | 说明 | 类型 | 可选值 | -| ---------- | --------------------------------- | ------ | ------ | -| this.item | 获取当前 index 对应的循环体数据; | Any | - | -| this.index | 当前物料在循环体中的 index | Number | - | - -## 2.5 工具类扩展描述(AA) - -用于描述物料开发过程中,自定义扩展或引入的第三方工具类(例如:lodash 及 moment),增强搭建基础协议的扩展性,提供通用的工具类方法的配置方案及调用 API。 - -| 参数 | 说明 | 类型 | 支持变量 | 默认值 | -| ------------------ | ------------------ | ---------------------------------------------------------------------------------------------------------------- | -------- | ------ | -| utils[] | 工具类扩展映射关系 | Array\<**UtilItem**\> | - | | -| *UtilItem*.name | 工具类扩展项名称 | String | - | | -| *UtilItem*.type | 工具类扩展项类型 | 枚举, `'npm'` (代表公网 npm 类型) / `'tnpm'` (代表阿里巴巴内部 npm 类型) / `'function'` (代表 Javascript 函数类型) | - | | -| *UtilItem*.content | 工具类扩展项内容 | [ComponentMap 类型](#22-组件映射关系 a) 或 [JSFunction](#2432事件函数类型 a) | - | | - -描述示例: - -```javascript -{ - utils: [{ - name: 'clone', - type: 'npm', - content: { - package: 'lodash', - version: '0.0.1', - exportName: 'clone', - subName: '', - destructuring: false, - main: '/lib/clone' - } - }, { - name: 'moment', - type: 'npm', - content: { - package: '@alifd/next', - version: '0.0.1', - exportName: 'Moment', - subName: '', - destructuring: true, - main: '' - } - }, { - name: 'recordEvent', - type: 'function', - content: { - type: 'JSFunction', - value: "function(logkey, gmkey, gokey, reqMethod) {\n goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod);\n}" - } - }] -} -``` - -出码结果: - -```javascript -import clone from 'lodash/lib/clone'; -import { Moment } from '@alifd/next'; - -export const recordEvent = function(logkey, gmkey, gokey, reqMethod) { - goldlog.record('/xxx.event.' + logkey, gmkey, gokey, reqMethod); -} - -... -``` - -扩展的工具类,用户可以通过统一的上下文 this.utils 方法获取所有扩展的工具类或自定义函数 ,例如:this.utils.moment、this.utils.clone。搭建协议中的使用方式如下所示: - -```javascript -{ - componentName: 'Div', - props: { - onClick: { - type: 'JSFunction, - value: 'function(){ this.utils.clone(this.state.data); }' - } - } -} -``` - -## 2.6 国际化多语言支持(AA) - -协议中用于描述国际化语料和组件引用国际化语料的规范,遵循集团国际化中台关于国际化语料规范定义。 - - -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| ---- | -------------- | ------ | ------ | ------ | -| i18n | 国际化语料信息 | Object | - | null | - - -描述示例: - -```json -{ - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China" - } - } -} -``` - -使用举例: - -```json -{ - "componentName": "Button", - "props": { - "text": { - "type": "i18n", - "key": "i18n-jwg27yo4" - } - } -} -``` - -```json -{ - "componentName": "Button", - "props": { - "text": "按钮", - "onClick": { - "type": "JSFunction", - "value": "function() {\ - console.log(this.i18n('i18n-jwg27yo4'));\ - }" - } - } -} -``` - -使用举例(已废弃) -```json -{ - "componentName": "Button", - "props": { - "text": { - "type": "JSExpression", - "value": "this.i18n['i18n-jwg27yo4']" - } - } -} -``` - -# 3 应用描述 - -面向开发者的,描述完整应用的 Schema 规范,用于规范化约束**低代码平台**对**完整应用**的**输出**,以及**出码模块**( Schema2Code) 或**运行时动态渲染框架**(预览)的**输入**。 - -## 3.1 结构描述 - -- version { String } 当前应用协议版本号 -- componentsMap { Array } 当前应用所有组件映射关系 -- componentsTree { Array } 描述应用所有页面、低代码组件的组件树 -- utils { Array } 应用范围内的全局自定义函数或第三方工具类扩展 -- css { string } 应用范围内的全局样式; -- config: { Object } 当前应用配置信息 -- meta: { Object } 当前应用元数据信息 -- dataSource: { Array } 当前应用的公共数据源 (待定) -- i18n { Object } 国际化语料 - - -完整应用描述举例: - -```json -{ - "version": "1.0.0", // 当前协议版本号 - "componentsMap": [{ // 依赖 npm 组件描述 - "componentName": "Button", - "package": "alife/next", - "version": "1.0.0", - "destructuring": true, - "exportName": "Select", - "subName": "Button" - }], - "componentsTree": [{ // 应用内页面、低代码组件描述 - "componentName": "Page", // 单个页面 - "fileName": "page_index", - "props": {}, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "meta": { // 页面元信息 - "title": "首页", // 页面标题描述 - "router": "/", // 页面路由 - "spmb": "abef21", // spm B 位 - "url": "https://fusion.design", // 页面访问地址 - "creator": "xxx", - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "children": [{ - "componentName": "Div", - "props": { - "className": "red", - }, - "children": [{ - "componentName": "Button", - "props": { - "type": "primary", - "valueBind": { // 变量绑定 - "type": "JSExpression", - "value": "this.state.user.name" - }, - "onClick": { // 动作绑定 - "type": "JSExpression", - "value": "function(e) { console.log(e.target.innerText) }", - } - }, - }] - }, { - "componentName": "Component", // 单个组件 - "fileName": "BasicLayout", // 组件名称 - "props": {}, - "css": "body {font-size: 12px;} .table { width: 100px;}", - "meta": { // 组件元信息 - "title": "导航组件", // 组件中文标题 - "description": "这是一个导航类组件...", // 组件描述 - "creator": "xxx", - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "children": [{ - "componentName": "Nav", - "props": { - "className": "red" - }, - "children": [{ - "componentName": "NavItem", - "props": {} - }] - }] - }] - }], - "utils": [{ - "name": "clone", - "type": "npm", - "content": { - "package": "lodash", - "version": "0.0.1", - "exportName": "clone", - "subName": "", - "destructuring": false, - "main": "/lib/clone" - } - }, { - "name": "beforeRequestHandler", - "type": "function", - "content": { - "type": "JSFunction", - "value": "function(){\n ... \n}" - } - }], - "css": "body {font-size: 12px;} .table { width: 100px;}", - "config": { // 当前应用配置信息 - "sdkVersion": "1.0.3", // 渲染模块版本 - "historyMode": "hash", // 浏览器路由:browser 哈希路由:hash - "container": "J_Container", - "layout": { - "componentName": "BasicLayout", - "props": { - "logo": "...", - "name": "测试网站" - }, - }, - "theme": { - // for Fusion use dpl defined - "package": "@alife/theme-fusion", - "version": "^0.1.0", - // for Antd use variable - "primary": "#ff9966" - } - }, - "meta": { // 应用元数据信息 - "name": "demo 应用", // 应用中文名称, - "git_group": "appGroup", // 应用对应 git 分组名 - "project_name": "app_demo", // 应用对应 git 的 project 名称 - "description": "这是一个测试应用", // 应用描述 - "spma": "spa23d", // 应用 spma A 位信息 - "gmt_create": "2020-02-11 00:00:00", // 创建时间 - "gmt_modified": "2020-02-11 00:00:00", // 修改时间 - ... - }, - "i18n": { - "zh-CN": { - "i18n-jwg27yo4": "你好", - "i18n-jwg27yo3": "中国" - }, - "en-US": { - "i18n-jwg27yo4": "Hello", - "i18n-jwg27yo3": "China" - } - } -} -``` - -## 3.2 文件目录 - -以下是推荐的应用目录结构,与标准源码 build-scripts 对齐,这里的目录结构是帮助理解应用级协议的设计,不做强约束 - -```html -├── META/ # 低代码元数据信息,用于多分支冲突解决、数据回滚等功能 -├── public/ # 静态文件,构建时会 copy 到 build/ 目录 -│ ├── index.html # 应用入口 HTML -│ └── favicon.png # Favicon -├── src/ -│ ├── components/ # 应用内的低代码业务组件 -│ │ └── guide-component/ -│ │ ├── index.js # 组件入口 -│ │ ├── components.js # 组件依赖的其他组件 -│ │ ├── schema.js # schema 描述 -│ │ └── index.scss # css 样式 -│ ├── pages/ # 页面 -│ │ └── home/ # Home 页面 -│ │ ├── index.js # 页面入口 -│ │ └── index.scss # css 样式 -│ ├── layouts/ -│ │ └── basic-layout/ # layout 组件名称 -│ │ ├── index.js # layout 入口 -│ │ ├── components.js # layout 组件依赖的其他组件 -│ │ ├── schema.js # layout schema 描述 -│ │ └── index.scss # layout css 样式 -│ ├── config/ # 配置信息 -│ │ ├── components.js # 应用上下文所有组件 -│ │ ├── routes.js # 页面路由列表 -│ │ └── app.js # 应用配置文件 -│ ├── utils/ # 工具库 -│ │ └── index.js # 应用第三方扩展函数 -│ ├── locales/ # [可选]国际化资源 -│ │ ├── en-US -│ │ └── zh-CN -│ ├── global.scss # 全局样式 -│ └── index.jsx # 应用入口脚本, 依赖 config/routes.js 的路由配置动态生成路由; -├── webpack.config.js # 项目工程配置,包含插件配置及自定义 webpack 配置等 -├── README.md -├── package.json -├── .editorconfig -├── .eslintignore -├── .eslintrc.js -├── .gitignore -├── .stylelintignore -└── .stylelintrc.js -``` - -## 3.3 应用级别 APIs -> 下文中 `xxx` 代指任意 API -### 3.3.1 路由 Router API - - this.location.`xxx` - - this.history.`xxx` - - this.match.`xxx` - -### 3.3.2 应用级别的公共函数或第三方扩展 - - this.utils.`xxx` - -### 3.3.3 国际化相关 API -| API | 函数签名 | 说明 | -| -------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------ | -| this.i18n | (i18nKey: string, params?: { [paramName: string]: string; }) => string | i18nKey 是语料的标识符,params 可选,是用来做模版字符串替换的。返回语料字符串 | -| this.getLocale | () => string | 返回当前环境语言 code | -| this.setLocale | (locale: string) => void | 设置当前环境语言 code | - -**使用范例:** -```json -{ - "componentsTree": [{ - "componentName": "Page", - "fileName": "Page1", - "props": {}, - "children": [{ - "componentName": "Div", - "props": {}, - "children": [{ - "componentName": "Button", - "props": { - "children": { - "type": "JSExpression", - "value": "this.i18n('i18n-hello')" - }, - "onClick": { - "type": "JSFunction", - "value": "function () { this.setLocale('en-US'); }" - } - }, - }, { - "componentName": "Button", - "props": { - "children": { - "type": "JSExpression", - "value": "this.i18n('i18n-chicken', { count: this.state.count })" - }, - }, - }] - }], - }], - "i18n": { - "zh-CN": { - "i18n-hello": "你好", - "i18n-chicken": "我有${count}只鸡" - }, - "en-US": { - "i18n-hello": "Hello", - "i18n-chicken": "I have ${count} chicken" - } - } -} -```