├── .codecov.yml ├── .eslintignore ├── .eslintrc.js ├── .fatherrc.ts ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .umirc.js ├── LICENSE ├── README.md ├── README.zh-CN.md ├── commitlint.js ├── docs ├── components.en-US.md ├── components.md ├── demos │ └── valueType.tsx ├── faq.md ├── getting-started.en-US.md ├── getting-started.md ├── index.en-US.md ├── index.md ├── intro.en-US.md ├── intro.md ├── pro-list.changelog.md └── schema.md ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── dooring │ ├── README.md │ ├── __tests__ │ │ └── dooring.test.js │ └── package.json ├── skeleton │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── component │ │ ├── Descriptions │ │ │ └── index.tsx │ │ ├── List │ │ │ └── index.tsx │ │ └── Result │ │ │ └── index.tsx │ │ ├── demos │ │ ├── descriptions.tsx │ │ ├── list.tsx │ │ └── result.tsx │ │ ├── index.tsx │ │ └── skeleton.md ├── utils │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ │ ├── array-move │ │ └── index.ts │ │ ├── conversionMomentValue │ │ └── index.ts │ │ ├── dateArrayFormatter │ │ └── index.tsx │ │ ├── hooks │ │ ├── useDebounceFn │ │ │ └── index.ts │ │ ├── useDeepCompareEffect │ │ │ └── index.ts │ │ ├── useDocumentTitle │ │ │ └── index.ts │ │ ├── useFetchData │ │ │ └── index.tsx │ │ └── usePrevious │ │ │ └── index.ts │ │ ├── index.tsx │ │ ├── isBrowser │ │ └── index.ts │ │ ├── isDeepEqualReact │ │ └── index.ts │ │ ├── isDropdownValueType │ │ └── index.ts │ │ ├── isImg │ │ └── index.ts │ │ ├── isNil │ │ └── index.ts │ │ ├── isUrl │ │ └── index.ts │ │ ├── merge │ │ └── index.ts │ │ ├── omitBoolean │ │ └── index.ts │ │ ├── omitUndefined │ │ └── index.ts │ │ ├── omitUndefinedAndEmptyArr │ │ └── index.ts │ │ ├── parseValueToMoment │ │ └── index.ts │ │ ├── runFunction │ │ └── index.ts │ │ ├── transformKeySubmitValue │ │ └── index.ts │ │ ├── typing.ts │ │ └── useMountMergeState │ │ └── index.ts └── x6-react │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ └── src │ ├── component │ └── Base │ │ └── index.tsx │ ├── demos │ └── base.tsx │ ├── index.tsx │ └── x6-react.md ├── public ├── CNAME ├── favicon.ico └── icon.png ├── scripts ├── bootstrap.js ├── checkDeps.js ├── createRelease.js ├── gen_less_entry.js ├── generateSizeLimit.js ├── issue.js ├── preDeploy.js ├── release.js ├── replaceLib.js ├── syncTNPM.js ├── utils │ ├── exec.js │ ├── getPackages.js │ └── isNextVersion.js └── verifyCommit.js ├── tests ├── __snapshots__ │ └── doc.test.ts.snap ├── card │ ├── __snapshots__ │ │ └── demo.test.ts.snap │ ├── demo.test.ts │ └── index.test.tsx ├── demo.tsx ├── descriptions │ ├── __snapshots__ │ │ ├── demo.test.ts.snap │ │ ├── editor.test.tsx.snap │ │ └── index.test.tsx.snap │ ├── demo.test.ts │ ├── editor.test.tsx │ └── index.test.tsx ├── doc.test.ts ├── field │ ├── __snapshots__ │ │ ├── demo.test.ts.snap │ │ ├── field.test.tsx.snap │ │ └── status.test.tsx.snap │ ├── datePick.test.tsx │ ├── demo.test.ts │ ├── field.test.tsx │ ├── fixtures │ │ └── demo.tsx │ └── status.test.tsx ├── form │ ├── __snapshots__ │ │ ├── base.test.tsx.snap │ │ ├── demo.test.ts.snap │ │ ├── proFormMoney.test.tsx.snap │ │ ├── schemaForm.test.tsx.snap │ │ └── upload.test.tsx.snap │ ├── base.test.tsx │ ├── demo.test.ts │ ├── drawerForm.test.tsx │ ├── fieldSet.test.tsx │ ├── formList.test.tsx │ ├── formitem.test.tsx │ ├── lightFilter.test.tsx │ ├── loginForm.test.tsx │ ├── modalForm.test.tsx │ ├── proFormMoney.test.tsx │ ├── queryFilter.test.tsx │ ├── schemaForm.test.tsx │ ├── stepFormTest.test.tsx │ └── upload.test.tsx ├── layout │ ├── __snapshots__ │ │ ├── demo.test.ts.snap │ │ ├── footer.test.tsx.snap │ │ ├── index.test.tsx.snap │ │ ├── mobile.test.tsx.snap │ │ ├── pageContainer.test.tsx.snap │ │ ├── pageHeaderWarp.test.tsx.snap │ │ ├── settingDrawer.test.tsx.snap │ │ └── waterMark.test.tsx.snap │ ├── compatible.test.tsx │ ├── defaultProps.ts │ ├── defaultSettings.ts │ ├── demo.test.ts │ ├── footer.test.tsx │ ├── getPageTitle.test.tsx │ ├── index.test.tsx │ ├── mobile.test.tsx │ ├── pageContainer.test.tsx │ ├── pageHeaderWarp.test.tsx │ ├── settingDrawer.test.tsx │ ├── settings.test.tsx │ └── waterMark.test.tsx ├── list │ ├── __snapshots__ │ │ ├── demo.test.ts.snap │ │ └── index.test.tsx.snap │ ├── demo.test.ts │ └── index.test.tsx ├── no-duplicated.ts ├── setupTests.js ├── skeleton │ ├── __snapshots__ │ │ ├── demo.test.ts.snap │ │ └── skeleton.test.tsx.snap │ ├── demo.test.ts │ └── skeleton.test.tsx ├── table │ ├── __snapshots__ │ │ ├── column.test.tsx.snap │ │ ├── demo.test.ts.snap │ │ ├── dragSort.test.tsx.snap │ │ ├── editor-table.test.tsx.snap │ │ ├── index.test.tsx.snap │ │ ├── listtoolbar.test.tsx.snap │ │ ├── search.test.tsx.snap │ │ └── valueEnum.test.tsx.snap │ ├── column.test.tsx │ ├── columnSetting.test.tsx │ ├── demo.test.ts │ ├── demo.tsx │ ├── dragSort.test.tsx │ ├── editor-table.test.tsx │ ├── filter.test.tsx │ ├── form.test.tsx │ ├── index.test.tsx │ ├── listtoolbar.test.tsx │ ├── mock.data.json │ ├── pagination.test.tsx │ ├── polling.test.tsx │ ├── search.test.tsx │ ├── selectKeys.test.tsx │ ├── valueEnum.test.tsx │ └── valueType.test.tsx ├── tsconfig.duplicate.json ├── util.ts └── utils │ ├── __snapshots__ │ └── index.test.tsx.snap │ └── index.test.tsx ├── theme ├── builtins │ └── Previewer.tsx ├── layout.less ├── layout.tsx └── useDarkreader.tsx ├── tsconfig.json ├── typings.d.ts ├── webpack.config.js └── yarn.lock /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | # Fail the status if coverage drops by >= 0.1% 6 | threshold: 0.1 7 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | # production 4 | dist 5 | /.vscode 6 | lib 7 | es 8 | .umi 9 | .github 10 | scripts 11 | webpack.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | rules: { 4 | 'import/no-extraneous-dependencies': 0, 5 | 'import/no-unresolved': 0, 6 | 'import/no-useless-constructor': 0 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.fatherrc.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | // utils must build before core 5 | // runtime must build before renderer-react 6 | // components dependencies order: form -> table -> list 7 | const headPkgs: string[] = ['utils', 'skeleton']; 8 | const tailPkgs = readdirSync(join(__dirname, 'packages')).filter( 9 | (pkg) => pkg.charAt(0) !== '.' && !headPkgs.includes(pkg), 10 | ); 11 | 12 | const type = process.env.BUILD_TYPE; 13 | 14 | let config = {}; 15 | 16 | if (type === 'lib') { 17 | config = { 18 | cjs: { type: 'babel', lazy: true }, 19 | esm: false, 20 | pkgs: [...headPkgs, ...tailPkgs], 21 | }; 22 | } 23 | 24 | if (type === 'es') { 25 | config = { 26 | cjs: false, 27 | esm: { 28 | type: 'babel', 29 | }, 30 | pkgs: [...headPkgs, ...tailPkgs], 31 | extraBabelPlugins: [ 32 | ['babel-plugin-import', { libraryName: 'antd', libraryDirectory: 'es', style: true }, 'antd'], 33 | [require('./scripts/replaceLib')], 34 | ], 35 | }; 36 | } 37 | 38 | export default config; 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | **/dist 11 | /.vscode 12 | **/**/lib/** 13 | **/**/es/** 14 | # misc 15 | .DS_Store 16 | npm-debug.log* 17 | yarn-error.log 18 | 19 | /coverage 20 | .idea 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | 29 | functions/mock 30 | .temp/** 31 | 32 | # umi 33 | .umi 34 | .umi-production 35 | 36 | # screenshot 37 | screenshot 38 | .firebase 39 | .eslintcache 40 | .changelog 41 | .changelog.md -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # .npmrc 2 | node-options=--max_old_space_size=8192 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | **/dist 6 | **/lib 7 | **/es 8 | **\__snapshots__\** 9 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | import { readdirSync } from 'fs'; 2 | import chalk from 'chalk'; 3 | import { join } from 'path'; 4 | 5 | const headPkgList = []; 6 | // utils must build before core 7 | // runtime must build before renderer-react 8 | const pkgList = readdirSync(join(__dirname, 'packages')).filter( 9 | (pkg) => pkg.charAt(0) !== '.' && !headPkgList.includes(pkg), 10 | ); 11 | 12 | const alias = pkgList.reduce((pre, pkg) => { 13 | pre[`xu-${pkg}`] = join(__dirname, 'packages', pkg, 'src'); 14 | return { 15 | ...pre, 16 | }; 17 | }, {}); 18 | 19 | console.log(`🌼 alias list \n${chalk.blue(Object.keys(alias).join('\n'))}`); 20 | 21 | const tailPkgList = pkgList 22 | .map((path) => [join('packages', path, 'src'), join('packages', path, 'src', 'components')]) 23 | .reduce((acc, val) => acc.concat(val), []); 24 | 25 | const isProduction = process.env.NODE_ENV === 'production'; 26 | 27 | const isDeploy = process.env.SITE_DEPLOY === 'TRUE'; 28 | 29 | export default { 30 | title: 'XuComponents', 31 | mode: 'site', 32 | logo: 'https://gw.alipayobjects.com/zos/antfincdn/upvrAjAPQX/Logo_Tech%252520UI.svg', 33 | extraBabelPlugins: [ 34 | [ 35 | 'import', 36 | { 37 | libraryName: 'antd', 38 | libraryDirectory: 'es', 39 | style: true, 40 | }, 41 | ], 42 | ], 43 | metas: [ 44 | { 45 | property: 'og:site_name', 46 | content: 'ProComponents', 47 | }, 48 | { 49 | 'data-rh': 'keywords', 50 | property: 'og:image', 51 | content: 'https://procomponents.ant.design/icon.png', 52 | }, 53 | { 54 | property: 'og:description', 55 | content: '🏆 让中后台开发更简单', 56 | }, 57 | { 58 | name: 'keywords', 59 | content: '中后台,admin,Ant Design,ant design,Table,react,alibaba', 60 | }, 61 | { 62 | name: 'description', 63 | content: '🏆 让中后台开发更简单 包含 table form 等多个组件。', 64 | }, 65 | { 66 | name: 'apple-mobile-web-app-capable', 67 | content: 'yes', 68 | }, 69 | { 70 | name: 'apple-mobile-web-app-status-bar-style', 71 | content: 'black-translucent', 72 | }, 73 | { 74 | name: 'theme-color', 75 | content: '#1890ff', 76 | }, 77 | ], 78 | alias: process.env === 'development' ? alias : {}, 79 | // 用于切换 antd 暗黑模式 80 | // antd: { 81 | // dark: true, 82 | // }, 83 | resolve: { 84 | includes: [...tailPkgList, 'docs'], 85 | }, 86 | locales: [ 87 | ['zh-CN', '中文'], 88 | ['en-US', 'English'], 89 | ], 90 | navs: { 91 | 'en-US': [ 92 | null, 93 | { 94 | title: 'GitHub', 95 | path: 'https://github.com/MrXujiang/best-cps', 96 | }, 97 | ], 98 | 'zh-CN': [ 99 | null, 100 | { 101 | title: 'GitHub', 102 | path: 'https://github.com/MrXujiang/best-cps', 103 | }, 104 | ], 105 | }, 106 | analytics: isProduction 107 | ? { 108 | ga: 'UA-173569162-1', 109 | } 110 | : false, 111 | hash: true, 112 | ssr: isDeploy ? {} : undefined, 113 | exportStatic: {}, 114 | targets: { 115 | chrome: 80, 116 | firefox: false, 117 | safari: false, 118 | edge: false, 119 | ios: false, 120 | }, 121 | theme: { 122 | '@s-site-menu-width': '258px', 123 | }, 124 | ignoreMomentLocale: true, 125 | headScripts: ['https://gw.alipayobjects.com/os/antfincdn/fdj3WlJd5c/darkreader.js'], 126 | links: 127 | process.env.NODE_ENV === 'development' 128 | ? ['https://gw.alipayobjects.com/os/lib/antd/4.6.6/dist/antd.css'] 129 | : [], 130 | externals: { darkreader: 'window.DarkReader' }, 131 | menus: { 132 | '/components': [ 133 | { 134 | title: '架构设计', 135 | children: ['components.md', 'schema.md'], 136 | }, 137 | { 138 | title: '通用', 139 | children: ['skeleton', 'x6-react'], 140 | }, 141 | ], 142 | '/en-US/components': [ 143 | { 144 | title: 'Architecture Design', 145 | children: ['components.en-US.md'], 146 | }, 147 | { 148 | title: 'General', 149 | children: ['skeleton', 'x6-react'], 150 | }, 151 | ], 152 | }, 153 | webpack5: {}, 154 | mfsu: !isDeploy ? {} : undefined, 155 | fastRefresh: {}, 156 | }; 157 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MrXujiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 在开发大型项目时, 我们通常会遇到同一工程依赖不同组件包, 同时不同的组件包之间还会相互依赖的问题, 那么如何管理组织这些依赖包就是一个迫在眉睫的问题. 3 | 4 | ![image.png](http://cdn.dooring.cn/dr/1633425666915.png) 5 | 6 | 我们目前已有的方案有: **Multirepo**(多个依赖包独立进行git管理) 和 **Monorepo**(所有依赖库完全放入一个项目工程). 7 | 8 | **Multirepo**的缺点在于每个库变更之后,需要发布到线上,然后在项目中重新安装, 打包, 发布, 最后才能更新,这样如果依赖关系越复杂就越难以维护。**Monorepo**最大的缺点就是不便于代码的复用和共享。 9 | 10 | 为了解决上述的问题, **lerna** 这款工具诞生了, 它可以方便的管理具有多个包的 **JavaScript** 项目。同时对于组件包的开发者和维护者, 为了让团队其他成员更好的理解和使用我们开发的组件, 搭建组件文档和 **demo** 就显得格外重要. 11 | 12 | ![image.png](http://cdn.dooring.cn/dr/1633426696834.png) 13 | 14 | 我们对以上提到的几点问题做一个总结: 15 | 16 | - 大型项目中如何管理组织依赖包及其版本问题 17 | - 如何高效低成本的搭建简单易用的组件文档 18 | - 如何配置eslint代码规范和代码提交规范 19 | 20 | 接下来我将针对以上问题一一来给出解答. 如果大家想看实际的案例, 可以参考: 21 | 22 | - [best-cps | 基于lerna + dumi搭建的多包管理实践](https://github.com/MrXujiang/best-cps) 23 | 24 | 相关采用 lerna 的项目: 25 | 26 | | home🏠 | demo✨ | doc📦 | tutorial | wiki | 27 | | ----------- | ----------- | ----------- | ----------- | ----------- | 28 | | [website](http://h5.dooring.cn) | [Demo](http://h5.dooring.cn/h5_plus) | [Document](http://h5.dooring.cn/doc) | [视频&Video](https://www.zhihu.com/zvideo/1406394315950653440) | [wiki](https://github.com/MrXujiang/h5-Dooring/wiki) 29 | 30 | ## 大型项目中如何管理组织依赖包及其版本问题 31 | 32 | 这个问题主要用我上面的提到的 **lerna** 工具来解决. 目前我们比较熟悉的 **babel**, **create-react-app**, **vue-cli** 等都使用了 **lerna**. 33 | 34 | 在没使用 **lerna** 时, 我们不同库的组织形式可能如下: 35 | 36 | ![image.png](http://cdn.dooring.cn/dr/1633429548344.png) 37 | 38 | 使用 **lerna** 之后的库组织结构: 39 | 40 | ![image.png](http://cdn.dooring.cn/dr/1633429780559.png) 41 | 42 | 以上两个是我做的简图, 基本可以对比出使用 **lerna** 前后的差异, **lerna** 的作用是把多个项目或模块拆分为多个 **packages** 放入一个git仓库进行管理。我们可以使用它提供的命令轻松的对不同项目进行管理 , 如下: 43 | 44 | - lerna boostrap 自动解决packages之间的依赖关系,对于packages内部的依赖会直接采用symlink的方式关联 45 | - lerna publish 依赖git检测文件改动,自动发布,管理版本号 46 | - lerna create 创建一个 lerna 管理的package包 47 | - lerna clean 删除所有包下面的node_modules目录,也可以删除指定包下面的node_modules 48 | 49 | 同时 **lerna** 还会根据 git 提交记录,自动生成 changelog. 当然 **lerna** 还提供了很多有用的命令, 大家感兴趣可以在官网学习. 50 | 51 | ## 如何高效低成本的搭建简单易用的组件文档 52 | 53 | 对于组件文档, 市面上也有很多开源的工具, 比如 vue-press, storybook, docz等, 因为我最近的项目多为 react, 这里我使用的是 dumi. 之前在分享实现滑动验证码组件的时候已经和大家分享的 dumi的使用, 大家可以参考我之前的文章: 54 | - [从零开发一款轻量级滑动验证码插件](https://juejin.cn/post/7007615666609979400) 55 | 56 | 以下是在 lerna 项目中集成 dumi 后的文档站点效果: 57 | 58 | ![image.png](http://cdn.dooring.cn/dr/1633431582693.png) 59 | 60 | ## 如何配置eslint代码规范和代码提交规范 61 | 62 | eslint 代码规范我想每个朋友都不陌生, 我们只需要安装对应的插件并编写对应规则的配置文件即可, 这里举一个简单的例子: 63 | 64 | ``` js 65 | // .eslintrc.js 66 | module.exports = { 67 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 68 | rules: { 69 | 'import/no-extraneous-dependencies': 0, 70 | 'import/no-unresolved': 0, 71 | }, 72 | }; 73 | ``` 74 | 配置好之后我们需要设置检测时机, 比如说时运行时检测还是提交时检测, 由于个人习惯和效率问题, 我采用了提交时检测, 也就是当开发者功能开发完成, 执行 git commit 的时候进行检测, 我们可以利用 githook 来做预提交检测, 这里需要在 package.json 文件中添加如下命令: 75 | ``` js 76 | "gitHooks": { 77 | "pre-commit": "npm run lint:js" 78 | }, 79 | ``` 80 | 配置好之后我们随便写一行不合规范的代码, 然后提交, 终端会显示如下信息: 81 | 82 | ![image.png](http://cdn.dooring.cn/dr/1633432745182.png) 83 | 84 | 从控制台可以发现代码不合规范的位置和原因, 如果我们没有做出调整, 代码就无法提交, 通过这样的方式可以提高代码质量和出错概率, 非常有长远价值. 85 | 86 | 同时上面提到了 githooks, 对于 githooks 的知识也非常有意思, 它可以帮我们在代码提交的不同阶段进行自定义操作, 比如代码提交前的检测, 代码提交信息规范等进行校验, 常用的 gtihooks 有: 87 | 88 | - pre-commit 89 | - prepare-commit-msg 90 | - commit-msg 91 | - post-commit 92 | - pre-rebase 93 | - post-merge 94 | - pre-receive 95 | - update 96 | 97 | 大家感兴趣的可以访问 https://githooks.com 获取更多有关 githooks的内容. 98 | 99 | 对于代码提交规范, 我们也需要做统一管理, 这样能让团队更直观的知道每一次提交的内容是什么, 尤其是多人协作的时候. 以下是几个常见的提交不规范的例子: 100 | 101 | ``` 102 | git commit -m '添加弹窗' 103 | git commit -m ':update 更新' 104 | git commit -m 'fix 修复一个bug' 105 | ``` 106 | 之所以会存在以上提交格式不统一或者提交信息难懂的问题, 都是因为缺少了规范的制约, 所以说对于大型项目或者多人协作的项目, 最好还是统一规范, 这样能提前避免很多不必要的麻烦. 107 | 108 | 要想实现对工程师提交信息的检测, 需要用到 commit-msg 这个 githooks, 具体配置如下: 109 | 110 | ```js 111 | "gitHooks": { 112 | "pre-commit": "npm run lint:js", 113 | "commit-msg": "node ./commitlint.js verify-commit" 114 | } 115 | ``` 116 | 117 | 剩下的就是 commitlint.js 做的事情了, 它是我编写的一个 nodejs 脚本, 用来检测用户提交的信息是否规范, 当然大家也可以基于这个脚本定义自己的提交规范, 具体效果如下: 118 | 119 | ![image.png](http://cdn.dooring.cn/dr/1633434527477.png) 120 | 121 | 我们可以看到, 当我们提交了一个不符合规范的信息之后, 终端控制台会打印如下提示信息并终止程序继续进行. 122 | 123 | 通过以上的配置, 团队不同成员的写的代码和提交信息都会非常统一和规范, 项目整体的质量也会得到一定的提升. 124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 在开发大型项目时, 我们通常会遇到同一工程依赖不同组件包, 同时不同的组件包之间还会相互依赖的问题, 那么如何管理组织这些依赖包就是一个迫在眉睫的问题. 3 | 4 | ![image.png](http://cdn.dooring.cn/dr/1633425666915.png) 5 | 6 | 我们目前已有的方案有: **Multirepo**(多个依赖包独立进行git管理) 和 **Monorepo**(所有依赖库完全放入一个项目工程). 7 | 8 | **Multirepo**的缺点在于每个库变更之后,需要发布到线上,然后在项目中重新安装, 打包, 发布, 最后才能更新,这样如果依赖关系越复杂就越难以维护。**Monorepo**最大的缺点就是不便于代码的复用和共享。 9 | 10 | 为了解决上述的问题, **lerna** 这款工具诞生了, 它可以方便的管理具有多个包的 **JavaScript** 项目。同时对于组件包的开发者和维护者, 为了让团队其他成员更好的理解和使用我们开发的组件, 搭建组件文档和 **demo** 就显得格外重要. 11 | 12 | ![image.png](http://cdn.dooring.cn/dr/1633426696834.png) 13 | 14 | 我们对以上提到的几点问题做一个总结: 15 | 16 | - 大型项目中如何管理组织依赖包及其版本问题 17 | - 如何高效低成本的搭建简单易用的组件文档 18 | - 如何配置eslint代码规范和代码提交规范 19 | 20 | 接下来我将针对以上问题一一来给出解答. 如果大家想看实际的案例, 可以参考: 21 | 22 | - [best-cps | 基于lerna + dumi搭建的多包管理实践](https://github.com/MrXujiang/best-cps) 23 | 24 | ## 大型项目中如何管理组织依赖包及其版本问题 25 | 26 | 这个问题主要用我上面的提到的 **lerna** 工具来解决. 目前我们比较熟悉的 **babel**, **create-react-app**, **vue-cli** 等都使用了 **lerna**. 27 | 28 | 在没使用 **lerna** 时, 我们不同库的组织形式可能如下: 29 | 30 | ![image.png](http://cdn.dooring.cn/dr/1633429548344.png) 31 | 32 | 使用 **lerna** 之后的库组织结构: 33 | 34 | ![image.png](http://cdn.dooring.cn/dr/1633429780559.png) 35 | 36 | 以上两个是我做的简图, 基本可以对比出使用 **lerna** 前后的差异, **lerna** 的作用是把多个项目或模块拆分为多个 **packages** 放入一个git仓库进行管理。我们可以使用它提供的命令轻松的对不同项目进行管理 , 如下: 37 | 38 | - lerna boostrap 自动解决packages之间的依赖关系,对于packages内部的依赖会直接采用symlink的方式关联 39 | - lerna publish 依赖git检测文件改动,自动发布,管理版本号 40 | - lerna create 创建一个 lerna 管理的package包 41 | - lerna clean 删除所有包下面的node_modules目录,也可以删除指定包下面的node_modules 42 | 43 | 同时 **lerna** 还会根据 git 提交记录,自动生成 changelog. 当然 **lerna** 还提供了很多有用的命令, 大家感兴趣可以在官网学习. 44 | 45 | ## 如何高效低成本的搭建简单易用的组件文档 46 | 47 | 对于组件文档, 市面上也有很多开源的工具, 比如 vue-press, storybook, docz等, 因为我最近的项目多为 react, 这里我使用的是 dumi. 之前在分享实现滑动验证码组件的时候已经和大家分享的 dumi的使用, 大家可以参考我之前的文章: 48 | - [从零开发一款轻量级滑动验证码插件](https://juejin.cn/post/7007615666609979400) 49 | 50 | 51 | ## 如何配置eslint代码规范和代码提交规范 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /commitlint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const yParser = require('yargs-parser'); 4 | const chalk = require('chalk'); 5 | const osLocale = require('os-locale'); 6 | 7 | // 截取命令行参数 8 | const args = yParser(process.argv.slice(2)); 9 | const option = args._[0]; 10 | 11 | const judeCommitResult = () => { 12 | // 提取commit信息 13 | const msgPath = process.env.GIT_PARAMS || process.env.HUSKY_GIT_PARAMS; 14 | const msg = require('fs').readFileSync(msgPath, 'utf-8').trim(); 15 | const commitRE = 16 | /^(((\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]) )?(revert: )?(feat|fix|docs|UI|refactor|⚡perf|workflow|build|CI|typos|chore|tests|types|wip|release|dep|locale)(\(.+\))?: .{1,50}/; 17 | 18 | if (!commitRE.test(msg)) { 19 | osLocale().then((locale) => { 20 | if (locale === 'zh-CN') { 21 | console.error( 22 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(`提交日志不符合规范`)}\n\n${chalk.red( 23 | ` 合法的提交日志格式如下(emoji 和 模块可选填):\n\n`, 24 | )} 25 | ${chalk.green(`💥 feat(模块): 添加了个很棒的功能`)} 26 | ${chalk.green(`🐛 fix(模块): 修复了一些 bug`)} 27 | ${chalk.green(`📝 docs(模块): 更新了一下文档`)} 28 | ${chalk.green(`🌷 UI(模块): 修改/优化了一下样式`)} 29 | ${chalk.green(`🔨 refactor(模块): 代码重构`)} 30 | ${chalk.green(`🏰 chore(模块): 对脚手架做了些更改`)} 31 | ${chalk.green(`🌐 locale(模块): 为国际化做了微小的贡献`)} 32 | ${chalk.red(`See https://github.com/MrXujiang/best-cps for more details.\n`)}`, 33 | ); 34 | } else { 35 | console.error( 36 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 37 | `invalid commit message format.`, 38 | )}\n\n${chalk.red( 39 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n`, 40 | )} 41 | ${chalk.green(`💥 feat(compiler): add 'comments' option`)} 42 | ${chalk.green(`🐛 fix(compiler): fix some bug`)} 43 | ${chalk.green(`📝 docs(compiler): add some docs`)} 44 | ${chalk.green(`🌷 UI(compiler): better styles`)} 45 | ${chalk.green(`🔨 refactor(compiler): code refactor`)} 46 | ${chalk.green(`🏰 chore(compiler): Made some changes to the scaffolding`)} 47 | ${chalk.green(`🌐 locale(compiler): Made a small contribution to internationalization`)}\n 48 | ${chalk.red(`See https://github.com/MrXujiang/best-cps for more details.\n`)}`, 49 | ); 50 | } 51 | 52 | process.exit(1); 53 | }); 54 | } 55 | }; 56 | 57 | switch (option) { 58 | case 'verify-commit': 59 | // eslint-disable-next-line global-require 60 | judeCommitResult(); 61 | break; 62 | 63 | default: 64 | if (args.h || args.help) { 65 | const details = ` 66 | Commands: 67 | ${chalk.cyan('verify-commit')} 检查 commit 提交的信息 68 | More: 69 | ${chalk.red(`See https://github.com/MrXujiang/best-cps.\n`)} 70 | `.trim(); 71 | console.log(details); 72 | } 73 | break; 74 | } 75 | -------------------------------------------------------------------------------- /docs/components.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Component Overview 3 | order: 0 4 | group: 5 | path: / 6 | nav: 7 | title: Component 8 | path: /components 9 | --- 10 | 11 | # Architecture Design 12 | 13 | ProComponents was developed to reduce the cost of implementing CRUD in the middle and backend, with the idea of reducing the necessary state maintenance and focusing more on the business. 14 | 15 | - [ProSkeleton](/components/skeleton) Page level skeleton screen 16 | -------------------------------------------------------------------------------- /docs/components.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 组件总览 3 | order: 0 4 | group: 5 | path: / 6 | nav: 7 | title: 组件 8 | path: /components 9 | --- 10 | 11 | # 架构设计 12 | 13 | ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面。 14 | 15 | - [ProSkeleton](/components/skeleton) 页面级别的骨架屏 16 | - [X6-React](/components/x6-react) x6的react版本 17 | -------------------------------------------------------------------------------- /docs/demos/valueType.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import type { ProFieldValueType } from '@ant-design/pro-utils'; 3 | 4 | const valueEnum = { 5 | all: { text: '全部', status: 'Default' }, 6 | open: { 7 | text: '未解决', 8 | status: 'Error', 9 | }, 10 | closed: { 11 | text: '已解决', 12 | status: 'Success', 13 | disabled: true, 14 | }, 15 | processing: { 16 | text: '解决中', 17 | status: 'Processing', 18 | }, 19 | }; 20 | 21 | const options = [ 22 | { value: `password`, label: `密码输入框`, initialValue: '123456' }, 23 | { value: `money`, label: `金额输入`, initialValue: '123456' }, 24 | { value: `textarea`, label: `文本域`, initialValue: '123456\n121212' }, 25 | { value: `date`, label: `日期`, initialValue: Date.now() }, 26 | { value: `dateTime`, label: `日期时间`, initialValue: Date.now() }, 27 | { value: `dateWeek`, label: `周`, initialValue: Date.now() }, 28 | { value: `dateMonth`, label: `月`, initialValue: Date.now() }, 29 | { value: `dateQuarter`, label: `季度输入`, initialValue: Date.now() }, 30 | { value: `dateYear`, label: `年份输入`, initialValue: Date.now() }, 31 | { value: `dateRange`, label: `日期区间`, initialValue: [Date.now(), Date.now()] }, 32 | { value: `dateTimeRange`, label: `日期时间区间`, initialValue: [Date.now(), Date.now()] }, 33 | { value: `time`, label: `时间`, initialValue: Date.now() }, 34 | { value: `timeRange`, label: `时间区间`, initialValue: [Date.now(), Date.now()] }, 35 | { value: `text`, label: `文本框`, initialValue: '123456' }, 36 | { value: `select`, label: `下拉框`, initialValue: 'open' }, 37 | { value: `checkbox`, label: `多选框`, initialValue: 'open' }, 38 | { value: `rate`, label: `星级组件`, initialValue: '' }, 39 | { value: `radio`, label: `单选框`, initialValue: 'open' }, 40 | { value: `radioButton`, label: `按钮单选框`, initialValue: 'open' }, 41 | { value: `progress`, label: `进度条`, initialValue: '10' }, 42 | { value: `percent`, label: `百分比组件`, initialValue: '20' }, 43 | { value: `digit`, label: `数字输入框`, initialValue: '200000' }, 44 | { value: `second`, label: `秒格式化`, initialValue: 20000 }, 45 | { 46 | value: `avatar`, 47 | label: `头像`, 48 | initialValue: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 49 | }, 50 | { value: `code`, label: `代码框`, initialValue: '# 2121' }, 51 | { value: `switch`, label: `开关`, initialValue: 'open' }, 52 | { value: `fromNow`, label: `相对于当前时间`, initialValue: Date.now() }, 53 | { 54 | value: `image`, 55 | label: `图片`, 56 | initialValue: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 57 | }, 58 | { value: `jsonCode`, label: `JSON代码框`, initialValue: '{ "name":"qixian" }' }, 59 | { 60 | value: `color`, 61 | label: `颜色选择器`, 62 | initialValue: '#1890ff', 63 | }, 64 | ]; 65 | 66 | type DataItem = { 67 | name: string; 68 | state: string; 69 | }; 70 | 71 | export default () => { 72 | const [valueType, setValueType] = useState('text'); 73 | return ( 74 | <> 75 | 76 | 77 | ); 78 | }; 79 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | order: 3 4 | group: 5 | path: / 6 | nav: 7 | title: FAQ 8 | path: /docs 9 | --- 10 | 11 | ## FAQ 12 | 13 | 以下整理了一些 ProComponents 社区常见的问题和官方答复,在提问之前建议找找有没有类似的问题。此外我们也维护了一个反馈较多 [how to use 标签](https://github.com/ant-design/pro-components/issues?q=is%3Aissue+label%3A%22%F0%9F%A4%B7%F0%9F%8F%BC+How+to+use%22+) 亦可参考。 14 | 15 | ### ProTable request 返回的数据格式可以自定义吗? 16 | 17 | 不行的,你可以在 request 中转化一下,或者写个拦截器。 18 | 19 | [示例](https://beta-pro.ant.design/docs/request-cn) 20 | 21 | ### 如何隐藏 ProTable 生成的搜索的 label? 22 | 23 | columns 的 title 支持 function 的,你可以这样写 24 | 25 | ```typescript 26 | title: (_, type) => { 27 | if (type === 'table') { 28 | return '标题'; 29 | } 30 | return null; 31 | }; 32 | ``` 33 | 34 | ### 我没法安装 `ProComponents` 和 `ProComponents` 的依赖,顺便提一句,我在中国大陆。 35 | 36 | 那啥,试试 [cnpm](http://npm.taobao.org/)和[yarn](https://www.npmjs.com/package/yarn)。 37 | 38 | ### `Form` 当中 `initialValues` 39 | 40 | `ProComponents` 底层也是封装的 [antd](https://ant.design/index-cn) ,所以用法也是和 [antd](https://ant.design/index-cn) 相同。注意 `initialValues` 不能被 `setState` 动态更新,你需要用 `setFieldsValue` 来更新。 `initialValues` 只在 `form` 初始化时生效且只生效一次,如果你需要异步加载推荐使用 `request`,或者 `initialValues ?
: null` 41 | 42 | ## 错误和警告 43 | 44 | 这里是一些你在使用 ProComponents 的过程中可能会遇到的错误和警告,但是其中一些并不是 ProComponents 的 bug。 45 | 46 | ### Cannot read property 'Provider' of undefined 47 | 48 | 请确保 antd 的版本 >= `4.11.1` 49 | -------------------------------------------------------------------------------- /docs/getting-started.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | order: 2 4 | group: 5 | path: / 6 | nav: 7 | title: Documentation 8 | path: /docs 9 | --- 10 | 11 | ## ProComponents 12 | 13 | ProComponents is a template component based on Ant Design that provides a higher level of abstraction support out of the box. It can significantly improve the efficiency of creating CRUD pages and focus more on them. 14 | 15 | - [X6-React](/components/x6-react) solves layout problems, provides out-of-the-box menu and breadcrumb functionality 16 | - [ProSkeleton](/components/skeleton) Page level skeleton screen 17 | 18 | ProComponents is focused on middle and backend CRUD and has a lot of pre-defined styles and behaviors. These behaviors and styles can be difficult to change, so if your business requires rich customization it is recommended to use Ant Design directly. 19 | 20 | ## Installation 21 | 22 | Currently each component of ProComponents is a separate package, you need to install the corresponding npm package in your project and use it. 23 | 24 | ## Using in a project 25 | 26 | Each package is a separate component package, and is used in the following example. 27 | 28 | All our packages use less for style management and easy theme customization. If you don't have less-loader you can try to import css from `dist`. 29 | 30 | ```tsx | pure 31 | import '@ant-design/pro-form/dist/form.css'; 32 | import '@ant-design/pro-table/dist/table.css'; 33 | import '@ant-design/pro-layout/dist/layout.css'; 34 | ``` 35 | 36 | It is recommended to use less, which allows for easy theme customization and on-demand loading. 37 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速开始 3 | order: 2 4 | group: 5 | path: / 6 | nav: 7 | title: 文档 8 | path: /docs 9 | --- 10 | 11 | ## ProComponents 12 | 13 | ProComponents 是基于 Ant Design 而开发的模板组件,提供了更高级别的抽象支持,开箱即用。可以显著的提升制作 CRUD 页面的效率,更加专注于页面。 14 | 15 | - [X6-React](/components/x6-react) 提供卡片切分以及栅格布局能力 16 | - [ProSkeleton](/components/skeleton) 页面级别的骨架屏 17 | 18 | 在使用之前可以查看一下典型的 Demo 来判断组件是否适合你们的业务。ProComponents 专注于中后台的 CRUD, 预设了相当多的样式和行为。这些行为和样式更改起来会比较困难,如果你的业务需要丰富的自定义建议直接使用 Ant Design。 19 | 20 | ## 安装 21 | 22 | 当前 ProComponents 每一个组件都是一个独立的包,你需要在你的项目中安装对应的 npm 包并使用。 23 | 24 | ```shell 25 | $ npm i @ant-design/pro-table --save 26 | ``` 27 | 28 | 我们所有的包都使用 less 来进行样式管理,方便进行主题的自定义。如果你没有 less-loader 可以尝试从 `dist` 中导入 css。 29 | 30 | ```tsx | pure 31 | import '@ant-design/pro-form/dist/form.css'; 32 | import '@ant-design/pro-table/dist/table.css'; 33 | import '@ant-design/pro-layout/dist/layout.css'; 34 | ``` 35 | 36 | 建议还是使用 less,可以方便进行主题自定义,也可以做到按需加载。 37 | -------------------------------------------------------------------------------- /docs/index.en-US.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ProComponents - Page level front-end components 3 | order: 10 4 | sidebar: false 5 | hero: 6 | title: ProComponents 7 | desc: 🏆 Make middle and backstage development easier 8 | actions: 9 | - text: 🥳 quick-start → 10 | link: /en-US/docs/getting-started 11 | 12 | features: 13 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziitmp/13668549-b393-42a2-97c3-a6365ba87ac2_w96_h96.png 14 | title: Easy to use 15 | desc: Wrapped in Ant Design to make it easier to use 16 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziik0f/487a2685-8f68-4c34-824f-e34c171d0dfd_w96_h96.png 17 | title: Ant Design 18 | desc: The same design system as Ant Design, seamlessly connects to antd project 19 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziip85/89434dcf-5f1d-4362-9ce0-ab8012a85924_w96_h96.png 20 | title: Internationalization 21 | desc: Provides complete internationalization language support, and connects to the Ant Design system 22 | - icon: https://gw.alipayobjects.com/mdn/rms_05efff/afts/img/A*-3XMTrwP85wAAAAAAAAAAAAABkARQnAQ 23 | title: preset style 24 | desc: The style is the same as antd, no need to change it, it's a natural fit 25 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziieuq/decadf3f-b53a-4c48-83f3-a2faaccf9ff7_w96_h96.png 26 | title: preset behavior 27 | desc: Less code, less bugs 28 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9zij2bh/67f75d56-0d62-47d6-a8a5-dbd0cb79a401_w96_h96.png 29 | title: TypeScript 30 | desc: Development with TypeScript, complete with type definition files 31 | 32 | footer: Open-source MIT Licensed | © 2017-present 33 | --- 34 | 35 | ## Component Board 36 | 37 | | Components | Downloads | Versions | 38 | | --- | --- | --- | 39 | | pro-layout | [![layout](https://img.shields.io/npm/dw/@ant-design/pro-layout.svg)](https://www.npmjs.com/package/@ant-design/pro-layout) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-layout.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-layout) | 40 | | pro-table | [![table](https://img.shields.io/npm/dw/@ant-design/pro-table.svg)](https://www.npmjs.com/package/@ant-design/pro-table) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-table.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-table) | 41 | | pro-field | [![field](https://img.shields.io/npm/dw/@ant-design/pro-field.svg)](https://www.npmjs.com/package/@ant-design/pro-field) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-field.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-field) | 42 | | pro-form | [![form](https://img.shields.io/npm/dw/@ant-design/pro-form.svg)](https://www.npmjs.com/package/@ant-design/pro-form) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-form.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-form) | 43 | | pro-skeleton | [![skeleton](https://img.shields.io/npm/dw/@ant-design/pro-skeleton.svg)](https://www.npmjs.com/package/@ant-design/pro-skeleton) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-skeleton.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-skeleton) | 44 | | pro-list | [![list](https://img.shields.io/npm/dw/@ant-design/pro-list.svg)](https://www.npmjs.com/package/@ant-design/pro-list) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-list.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-list) | 45 | | pro-card | [![card](https://img.shields.io/npm/dw/@ant-design/pro-card.svg)](https://www.npmjs.com/package/@ant-design/pro-card) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-card.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-card) | 46 | | pro-descriptions | [![descriptions](https://img.shields.io/npm/dw/@ant-design/pro-card.svg)](https://www.npmjs.com/package/@ant-design/pro-descriptions) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-descriptions.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-descriptions) | 47 | 48 | ## 🖥 Browser Compatibility 49 | 50 | - Modern browsers and Internet Explorer 11 (with [polyfills](https://stackoverflow.com/questions/57020976/polyfills-in-2019-for-ie11)) 51 | - [Electron](https://www.electronjs.org/) 52 | 53 | | [![edge](https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![Edge](https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![chrome](https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![safari](https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![electron_48x48](https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png)](http://godban.github.io/browsers-support-badges/) | 54 | | --- | --- | --- | --- | --- | 55 | | IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 56 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ProComponents - 页面级别的前端组件 3 | order: 10 4 | sidebar: false 5 | hero: 6 | title: ProComponents 7 | desc: 🏆 让中后台开发更简单 8 | actions: 9 | - text: 🥳 快速开始 → 10 | link: /docs/getting-started 11 | 12 | features: 13 | - icon: https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*43rfS4dD0MUAAAAAAAAAAABkARQnAQ 14 | title: 简单易用 15 | desc: 在 Ant Design 上进行了自己的封装,更加易用 16 | - icon: https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg 17 | title: Ant Design 18 | desc: 与 Ant Design 设计体系一脉相承,无缝对接 antd 项目 19 | - icon: https://gw.alipayobjects.com/zos/antfincdn/CPoxyg4J2d/geography.png 20 | title: 国际化 21 | desc: 提供完备的国际化,与 Ant Design 体系打通 22 | - icon: https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*cY0tTr8q3Y4AAAAAAAAAAABkARQnAQ 23 | title: 预设样式 24 | desc: 样式风格与 antd 一脉相承,无需魔改,浑然天成 25 | - icon: https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*abGUQKUocSMAAAAAAAAAAABkARQnAQ 26 | title: 预设行为 27 | desc: 更少的代码,更少的 Bug 28 | - icon: https://gw.alipayobjects.com/zos/antfincdn/Eb8IHpb9jE/Typescript_logo_2020.svg 29 | title: TypeScript 30 | desc: 使用 TypeScript 开发,提供完整的类型定义文件 31 | 32 | footer: Open-source MIT Licensed | © 2017-present 33 | --- 34 | 35 | ## 组件看板 36 | 37 | | 组件 | 下载量 | 版本 | 38 | | --- | --- | --- | 39 | | pro-layout | [![layout](https://img.shields.io/npm/dw/@ant-design/pro-layout.svg)](https://www.npmjs.com/package/@ant-design/pro-layout) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-layout.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-layout) | 40 | | pro-table | [![table](https://img.shields.io/npm/dw/@ant-design/pro-table.svg)](https://www.npmjs.com/package/@ant-design/pro-table) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-table.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-table) | 41 | | pro-field | [![field](https://img.shields.io/npm/dw/@ant-design/pro-field.svg)](https://www.npmjs.com/package/@ant-design/pro-field) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-field.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-field) | 42 | | pro-form | [![form](https://img.shields.io/npm/dw/@ant-design/pro-form.svg)](https://www.npmjs.com/package/@ant-design/pro-form) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-form.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-form) | 43 | | pro-skeleton | [![skeleton](https://img.shields.io/npm/dw/@ant-design/pro-skeleton.svg)](https://www.npmjs.com/package/@ant-design/pro-skeleton) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-skeleton.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-skeleton) | 44 | | pro-list | [![list](https://img.shields.io/npm/dw/@ant-design/pro-list.svg)](https://www.npmjs.com/package/@ant-design/pro-list) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-list.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-list) | 45 | | pro-card | [![card](https://img.shields.io/npm/dw/@ant-design/pro-card.svg)](https://www.npmjs.com/package/@ant-design/pro-card) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-card.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-card) | 46 | | pro-descriptions | [![descriptions](https://img.shields.io/npm/dw/@ant-design/pro-card.svg)](https://www.npmjs.com/package/@ant-design/pro-descriptions) | [![npm package](https://img.shields.io/npm/v/@ant-design/pro-descriptions.svg?style=flat-square?style=flat-square)](https://www.npmjs.com/package/@ant-design/pro-descriptions) | 47 | 48 | ## 🖥 浏览器兼容性 49 | 50 | - 现代浏览器和 Internet Explorer 11 (with [polyfills](https://stackoverflow.com/questions/57020976/polyfills-in-2019-for-ie11)) 51 | - [Electron](https://www.electronjs.org/) 52 | 53 | | [![edge](https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![Edge](https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![chrome](https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![safari](https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png)](http://godban.github.io/browsers-support-badges/) | [![electron_48x48](https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png)](http://godban.github.io/browsers-support-badges/) | 54 | | --- | --- | --- | --- | --- | 55 | | IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | 56 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 简介 3 | order: 1 4 | group: 5 | path: / 6 | nav: 7 | title: 文档 8 | order: 1 9 | path: /docs 10 | --- 11 | 12 | ![banner](https://gw.alipayobjects.com/zos/antfincdn/7VBnGHwjaW/bianzu%2525202.svg) 13 | 14 | ## ProComponents 的理念 15 | 16 | Ant Design 定义了基础的设计规范,对应也提供了大量的基础组件。但是对于中后台类应用,我们希望提供更高程度的抽象,提供更上层的设计规范,并且对应提供相应的组件使得开发者可以快速搭建出高质量的页面。 17 | 18 | 在 ProComponents 中我们内置了一系列的设计规范,预设了常用的逻辑。在这个基础上我们同样提供了灵活的支持,比如对于 ProTable 来说你也可以把它完全当做 Ant Design 的 Table 来用,对于 ProForm 来说你也可以直接使用 Ant Design 的基础组件或者你的自定义组件。我们希望通过 Pro 系列组件提供快速高效大家高质量中后台应用的能力,进一步扩展 Ant Design 的能力,欢迎使用并提出宝贵的意见。 19 | 20 | ## 设计思路 21 | 22 | 对于几乎所有的业务来说,我们做的其实就是根据一个状态定义一系列的行为,以上面的 table 为例,首先我们需要一个状态 `dataSource` 用于存储从服务器请求的数据,为了优化体验,我们还需要一个 `loading`。于是我们就有了一系列的行为,我们需要先设置 `loading=true`,然后发起网络请求,网络请求完成之后就 设置 `dataSource` 为请求回来的数据,`loading=false`,一个网络请求就完成了,虽然非常简单,但是一个业务系统有相当多的表格,每个表格都定义这么一次,这个工作量就非常大了。 23 | 24 | 如果要重新请求网络,我们就需要封装一下行为,将以上的行为封装成一个方法,点击一下重新加载数据,如果你有分页,那么就需要新的变量 page,我们在重新请求之前需要去根据需要来判断一下是否将页面重置为第一页,这又引入了一个变量。如果你的表格还要控制每页的数量,那么将会更加繁杂。这种重复性的劳动会浪费掉我们的很多时间。 25 | 26 | ### 一个状态加一系列行为 27 | 28 | 以上的逻辑几乎存在于所有中后台开发中,每增加一个状态我们就需要一系列的行为来进行管理,每个行为如果耦合了太多的状态也会复杂到无以复加。 29 | 30 | 碰上这种情况,几乎所有程序员都会想办法进行分层,基于同样的思路,ProTable 希望抽象出一层来解决掉复杂状态的问题,table 中最常用的状态就是 `loading` 和 `dataSource`,包括扩展的 `page`,`pageSize` 其实都是服务于网络状态,于是 table 抽象出了一个 `request` 的 api,在其中封装了 loading 和 dataSource 状态以及他们所有的行为,上一页,下一页,重新刷新,修改每页大小等行为。 31 | 32 | 这种封装模式可以让前端从各种状态管理中脱身出来,专注于业务开发,也不需要 dva,redux 等数据流的方案,更加符合直觉。开发者只需要定义一个状态,重型组件会自动生成一系列行为。 33 | 34 | > 为了渐进式使用我们也提供了与 Ant Design 相同的 api,完全可以降级成为一个 Ant Design 的 table 使用。 35 | 36 | ### 一个组件 ≈ 一个页面 37 | 38 | 重型组件区别于传统组件有个很大的不同,重型组件在抽象时是将其当成一个页面来进行处理,所以 ProTable 会支持网络请求和自动生成查询表单,而 ProLayout 会支持自动生成菜单,两者都基于同样的思想也就是提供页面级别的抽象。 39 | 40 | 一个列表页应该可以用 ProLayout + ProTable 完成,一个编辑页应该使用 ProLayout + ProForm 完成,详情页可以用 ProLayout + ProDescriptions 完成。 一个页面在开发工程中只需要关注几个重型组件,降低心智负担,专注于更核心的业务逻辑。 41 | 42 | ### 设计与样式 43 | 44 | 在实际开发中我们也经常会碰到一些设计问题,比如经典的按钮应该放在左面还是右面,查询表单怎么布局,日期怎么格式化,数字的对齐问题,在重型组件中都进行了抽象,对于各种行为与样式我们都经过了设计师的讨论与设计可以达到默认好看及好用。 45 | 46 | 如果你还是想自定义相关渲染可以通过自定义 valueType 的方式来实现。默认的不一定是最好的,但是一定不差,如果你要自定义最好考虑一下投入产出比,毛坯房里雕花真的好吗? 47 | 48 | ## 参与贡献 49 | 50 | 我们非常欢迎你的贡献,你可以通过以下方式和我们一起共建 :smiley:: 51 | 52 | - 在你的公司或个人项目中使用 Ant Design Pro,umi 和 ProComponents。 53 | - 通过 [Issue](http://github.com/ant-design/pro-components/issues) 报告 bug 或进行咨询。 54 | - 提交 [Pull Request](http://github.com/ant-design/pro-components/pulls) 改进 ProComponents 的代码。 55 | 56 | ### 脚手架概览 57 | 58 | 当我们 clone 完项目之后会看到如下的目录结构。 59 | 60 | ```bash 61 | - .dumi * dumi 的相关配置,主要是主题等 62 | - .github * github 的 action 和相关的 issue 配置 63 | - docs * 存放公用的文档 64 | - packages * 我们维护的包, 如果你想贡献代码,这里是你最需要关注的 65 | - README.md * 展示在 github 主页的代码 66 | - tests * 编写测试用例的地方 67 | - public * 部署官网所用的静态文件 68 | - scripts * 开发或者部署所用的脚本 69 | - .prettierrc.js * prettier 的相关配置 70 | - .eslintrc.js * eslint 的配置 71 | - .fatherrc.ts * 编译脚手架的配置 72 | - .umirc.js * dumi 的核心配置 73 | - webpack.config.js * 编译 umd 包的配置文件 74 | - jest.config.js * 测试环境的配置 75 | - lerna.json * 多包的配置 76 | - package.json * 项目的配置 77 | - tsconfig.json * typescript 的配置 78 | - yarn.lock * 依赖 lock 文件 79 | ``` 80 | 81 | `coverage` 和 `.umi` 这两个文件夹比较特殊,`coverage` 是测试覆盖率文件,在跑完测试覆盖率后才会出现,`.umi` 是运行时的一些临时文件,在执行 `npm run start` 时生成。 82 | 83 | ### 源码概览 84 | 85 | 在 packages 文件夹中包含了我们所有的组件,每个组件一般都有一个 `src`,`package.json` 和 `README.md`。`package.json` 和 `README.md` 可以在新建文件夹后通过执行 `npm run bootstrap` 来生成。 86 | 87 | `src` 中就是我们真正的源码,我们约定 `src` 下会有 demos 文件夹里面会存储所有的 demo,并且 `${包名}.md` 的文件用于介绍这个组件,同时引入 demo 和 API 文档。 88 | 89 | > 我们使用了 dumi 的语法,要求全部使用外置组件,用 code 引入,调试起来会更加方便。 90 | 91 | ### 风格指南 92 | 93 | 我们使用自动化代码格式化软件 [`Prettier`](https://prettier.io/)。 对代码做出更改后,运行 `npm run prettier`。当然我们更推荐 prettier 的插件,随时格式化代码。 94 | 95 | > 我们的 CI 会检查代码是否被 prettier,在提交代码前最好执行一下 `npm run prettier`。 96 | 97 | 之后,`linter` 会捕获代码中可能出现的多数问题。 你可以运行 `npm run lint` 来检查代码风格状态。 98 | 99 | 不过,`linter` 也有不能搞定的一些风格。如果有些东西不确定,请查看 [Airbnb’s Style Guide](https://github.com/airbnb/javascript) 来指导自己。 100 | 101 | ### 开发工作流 102 | 103 | 我们使用了 [monorepo](https://danluu.com/monorepo/) 的方式来管理我们的仓库,仓库中包含多个独立的包,以便于更改可以一起联调,这样可以一起跑测试用例,如果变更出现问题,我们可以很快的定位到问题。 104 | 105 | 因为使用了 monorepo ,我们要求必须要使用 yarn 来安装依赖。[`workspace`](https://classic.yarnpkg.com/en/docs/workspaces#search) 可以帮助我们在多个包中共享依赖。 106 | 107 | 安装完成后你可以使用以下命令: 108 | 109 | - `yarn start` 预览你的改动 110 | - `yarn lint` 检查代码风格 111 | - `yarn tsc` 检查 TypeScript 是否符合规范 112 | - `yarn test` 测试代码是否可以通过测试用例 113 | - `yarn test:coverage` 测试仓库的测试覆盖率 114 | - `yarn build` 编译当前组件库 115 | 116 | 我们建议运行 `yarn test` 或前文提及的 linter 以确保你的代码变更有没有影响原有功能,同时保证你写的每行代码都被正确的测试到,不管怎样这样都会提升组件库的整体质量。 117 | 118 | 如果你增加了一个新功能,请添加测试后再提交 pr,这样我们能确保以后你的代码不出问题。 119 | 120 | ### 一些约定 121 | 122 | ProComponents 基于 antd 之上来开发,为了与 antd 的生态保持兼容性,我们要求覆盖 antd 的样式必须要使用 `.@{ant-prefix}` 变量来生成类名,在 js 中使用如下代码来配置实现。 123 | 124 | ```tsx | pure 125 | const { getPrefixCls } = useContext(ConfigProvider.ConfigContext); 126 | const prefixCls = getPrefixCls('pro-${包名}'); 127 | ``` 128 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 通用配置总览 3 | order: 1 4 | group: 5 | path: / 6 | nav: 7 | title: 组件 8 | path: /components 9 | --- 10 | 11 | # 通用配置 12 | 13 | 在 ProComponents 我们在组件使用了与 table 的相同的定义,同时扩展了部分字段。让其可以满足更多需求。 14 | 15 | | 字段名称 | 类型 | 说明 | 16 | | --- | --- | --- | 17 | | `key` | `React.key` | 确定这个列的唯一值,一般用于 dataIndex 重复的情况 | 18 | | `dataIndex` | `React.key` \| `React.key[]` | 与实体映射的 key,数组会被转化 `[a,b] => Entity.a.b` | 19 | | `valueType` | `ProFieldValueType` | 数据的渲渲染方式,我们自带了一部分,你可以可以自定义 valueType | 20 | | `title` | `ReactNode` \|`(props,type,dom)=> ReactNode` | 标题的内容,在 form 中是 label | 21 | | `tooltip` | `string` | 会在 title 旁边展示一个 icon,鼠标浮动之后展示 | 22 | | `valueEnum` | `(Entity)=> ValueEnum` \| `ValueEnum` | 支持 object 和 Map,Map 是支持其他基础类型作为 key | 23 | | `fieldProps` | `(form,config)=>fieldProps`\| `fieldProps` | 传给渲染的组件的 props,自定义的时候也会传递 | 24 | | `formItemProps` | `(form,config)=>formItemProps` \| `formItemProps` | 传递给 Form.Item 的配置 | 25 | | `renderText` | `(text: any, record: Entity, index: number, action: ProCoreActionType) => any` | 修改的数据是会被 valueType 定义的渲染组件消费 | 26 | | `render` | `(dom,entity,index, action, schema) => React.ReactNode` | 自定义只读模式的 dom,`render` 方法只管理的只读模式,编辑模式需要使用 `renderFormItem` | 27 | | `renderFormItem` | `(schema,config,form) => React.ReactNode` | 自定义编辑模式,返回一个 ReactNode,会自动包裹 value 和 onChange | 28 | | `request` | `(params,props) => Promise<{label,value}[]>` | 从远程请求网络数据,一般用于选择类组件 | 29 | | `params` | `Record` | 额外传递给 `request` 的参数,组件不做处理,但是变化会引起`request` 重新请求数据 | 30 | | `hideInForm` | `boolean` | 在 Form 中隐藏 | 31 | | `hideInTable` | `boolean` | 在 Table 中隐藏 | 32 | | `hideInSearch` | `boolean` | 在 Table 的查询表单中隐藏 | 33 | | `hideInDescriptions` | `boolean` | 在 descriptions 中隐藏 | 34 | 35 | ## valueType 36 | 37 | valueType 是 ProComponents 的灵魂,ProComponents 会根据 valueType 来映射成不同的表单项。以下是支持的常见表单项: 38 | 39 | | valueType | 说明 | 40 | | --------------- | ---------------------------- | 41 | | `password` | 密码输入框 | 42 | | `money` | 金额输入框 | 43 | | `textarea` | 文本域 | 44 | | `date` | 日期 | 45 | | `dateTime` | 日期时间 | 46 | | `dateWeek` | 周 | 47 | | `dateMonth` | 月 | 48 | | `dateQuarter` | 季度输入 | 49 | | `dateYear` | 年份输入 | 50 | | `dateRange` | 日期区间 | 51 | | `dateTimeRange` | 日期时间区间 | 52 | | `time` | 时间 | 53 | | `timeRange` | 时间区间 | 54 | | `text` | 文本框 | 55 | | `select` | 下拉框 | 56 | | `checkbox` | 多选框 | 57 | | `rate` | 星级组件 | 58 | | `radio` | 单选框 | 59 | | `radioButton` | 按钮单选框 | 60 | | `progress` | 进度条 | 61 | | `percent` | 百分比组件 | 62 | | `digit` | 数字输入框 | 63 | | `second` | 秒格式化 | 64 | | `avatar` | 头像 | 65 | | `code` | 代码框 | 66 | | `switch` | 开关 | 67 | | `fromNow` | 相对于当前时间 | 68 | | `image` | 图片 | 69 | | `jsonCode` | 代码框,但是带了 json 格式化 | 70 | | `color` | 颜色选择器 | 71 | 72 | 这里 demo 可以来了解一下各个 valueType 的展示效果。 73 | 74 | ### 传入 function 75 | 76 | 只有一个值并不能表现很多类型,`progress` 就是一个很好的例子。所以我们支持传入一个 function。你可以这样使用: 77 | 78 | ```tsx |pure 79 | const columns = { 80 | title: '进度', 81 | key: 'progress', 82 | dataIndex: 'progress', 83 | valueType: (item: T) => ({ 84 | type: 'progress', 85 | status: item.status !== 'error' ? 'active' : 'exception', 86 | }), 87 | }; 88 | ``` 89 | 90 | ### 支持的返回值 91 | 92 | #### progress 93 | 94 | ```js 95 | return { 96 | type: 'progress', 97 | status: 'success' | 'exception' | 'normal' | 'active', 98 | }; 99 | ``` 100 | 101 | #### money 102 | 103 | ```js 104 | return { type: 'money', locale: 'en-Us' }; 105 | ``` 106 | 107 | #### percent 108 | 109 | ```js 110 | return { type: 'percent', showSymbol: true | false, precision: 2 }; 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { readdirSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | const pkgList = readdirSync(join(__dirname, './packages')).filter((pkg) => pkg.charAt(0) !== '.'); 5 | 6 | const moduleNameMapper = {}; 7 | 8 | pkgList.forEach((shortName) => { 9 | const name = `@ant-design/pro-${shortName}`; 10 | moduleNameMapper[name] = join(__dirname, `./packages/${shortName}/src`); 11 | }); 12 | 13 | module.exports = { 14 | collectCoverageFrom: [ 15 | 'packages/**/src/**/*.{ts,tsx}', 16 | '!packages/**/src/demos/**', 17 | '!packages/**/src/**/demos/**', 18 | '!packages/utils/src/isDeepEqualReact/*.{ts,tsx}', 19 | ], 20 | moduleNameMapper, 21 | testURL: 22 | 'http://localhost?navTheme=realDark&layout=mix&primaryColor=daybreak&splitMenus=false&fixedHeader=true', 23 | verbose: true, 24 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 25 | extraSetupFiles: ['./tests/setupTests.js'], 26 | globals: { 27 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "changelog": { 6 | "repo": "MrXujiang/best-cps", 7 | "cacheDir": ".changelog" 8 | }, 9 | "ignoreChanges": [ 10 | "**/*.md", 11 | "**/*.test.ts", 12 | "**/*.e2e.ts", 13 | "**/demos/**", 14 | "**/fixtures/**", 15 | "**/dist/**", 16 | "**/lib/**", 17 | "**/es/**", 18 | "**/test/**" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/dooring/README.md: -------------------------------------------------------------------------------- 1 | # `dooring` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const dooring = require('dooring'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/dooring/__tests__/dooring.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const dooring = require('..'); 4 | 5 | describe('dooring', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/dooring/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dooring", 3 | "version": "1.0.0", 4 | "description": "> TODO: description", 5 | "author": "xujiang ", 6 | "homepage": "https://github.com/MrXujiang/best-cps#readme", 7 | "license": "ISC", 8 | "main": "lib/dooring.js", 9 | "directories": { 10 | "lib": "lib", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "lib" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/MrXujiang/best-cps.git" 19 | }, 20 | "scripts": { 21 | "test": "echo \"Error: run tests from root\" && exit 1" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/MrXujiang/best-cps/issues" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/skeleton/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXujiang/best-cps/16bc24f0b314ee3ef0a4ea4aef1e4028b862908d/packages/skeleton/CHANGELOG.md -------------------------------------------------------------------------------- /packages/skeleton/README.md: -------------------------------------------------------------------------------- 1 | # xu-skeleton 2 | 3 | > xu-skeleton. 4 | 5 | See our website [@ant-design/pro-skeleton](https://github.com/MrXujiang/best-cps) for more information. 6 | 7 | ## Install 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install --save xu-skeleton 13 | ``` 14 | 15 | or using yarn: 16 | 17 | ```bash 18 | $ yarn add xu-skeleton 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/skeleton/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xu-skeleton", 3 | "version": "1.0.2", 4 | "description": "xu-skeleton", 5 | "keywords": [ 6 | "antd", 7 | "react-component" 8 | ], 9 | "sideEffects": [ 10 | "*.less" 11 | ], 12 | "homepage": "https://github.com/MrXujiang/best-cps/tree/master/packages/skeleton#readme", 13 | "bugs": "https://github.com/MrXujiang/best-cps/issues", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/MrXujiang/best-cps" 17 | }, 18 | "license": "MIT", 19 | "main": "lib/index.js", 20 | "module": "es/index.js", 21 | "types": "lib/index.d.ts", 22 | "files": [ 23 | "lib", 24 | "es", 25 | "dist" 26 | ], 27 | "browserslist": [ 28 | "last 2 versions", 29 | "Firefox ESR", 30 | "> 1%", 31 | "ie >= 11" 32 | ], 33 | "dependencies": { 34 | "use-media-antd-query": "^1.0.6" 35 | }, 36 | "peerDependencies": { 37 | "antd": "4.x", 38 | "react": ">=16.9.0" 39 | }, 40 | "publishConfig": { 41 | "access": "public" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/skeleton/src/component/Result/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Skeleton, Card, Space } from 'antd'; 3 | import { PageHeaderSkeleton } from '../List'; 4 | 5 | type ResultPageSkeletonProps = { 6 | active?: boolean; 7 | pageHeader?: false; 8 | }; 9 | 10 | const ResultPageSkeleton: React.FC = ({ active = true, pageHeader }) => ( 11 |
16 | {pageHeader !== false && } 17 | 18 |
27 | 33 | 34 | 35 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | ); 47 | 48 | export default ResultPageSkeleton; 49 | -------------------------------------------------------------------------------- /packages/skeleton/src/demos/descriptions.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProSkeleton from 'xu-skeleton'; 3 | 4 | export default () => { 5 | return ( 6 |
12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/skeleton/src/demos/list.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProSkeleton from 'xu-skeleton'; 3 | 4 | export default () => ( 5 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /packages/skeleton/src/demos/result.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProSkeleton from 'xu-skeleton'; 3 | 4 | export default () => ( 5 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /packages/skeleton/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { ListPageSkeletonProps } from './component/List'; 3 | import ListPageSkeleton, { 4 | PageHeaderSkeleton, 5 | ListToolbarSkeleton, 6 | ListSkeleton, 7 | ListSkeletonItem, 8 | } from './component/List'; 9 | import ResultPageSkeleton from './component/Result'; 10 | import type { DescriptionsPageSkeletonProps } from './component/Descriptions'; 11 | import DescriptionsPageSkeleton, { 12 | TableItemSkeleton, 13 | DescriptionsSkeleton, 14 | TableSkeleton, 15 | } from './component/Descriptions'; 16 | 17 | const PageSkeleton: React.FC< 18 | ListPageSkeletonProps & 19 | DescriptionsPageSkeletonProps & { 20 | type?: 'list' | 'result' | 'descriptions'; 21 | active?: boolean; 22 | } 23 | > = ({ type = 'list', ...rest }) => { 24 | if (type === 'result') { 25 | return ; 26 | } 27 | if (type === 'descriptions') { 28 | return ; 29 | } 30 | return ; 31 | }; 32 | 33 | export { 34 | ListPageSkeleton, 35 | ListSkeleton, 36 | ListSkeletonItem, 37 | PageHeaderSkeleton, 38 | ListToolbarSkeleton, 39 | DescriptionsSkeleton, 40 | TableSkeleton, 41 | TableItemSkeleton, 42 | }; 43 | 44 | export default PageSkeleton; 45 | -------------------------------------------------------------------------------- /packages/skeleton/src/skeleton.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: ProSkeleton - 骨架屏 3 | group: 4 | path: / 5 | nav: 6 | title: 组件 7 | path: /components 8 | --- 9 | 10 | # ProSkeleton 11 | 12 | > 该组件为内部组件,请勿直接使用。 13 | 14 | 页面级别的骨架屏,不支持自定义 15 | 16 | ## 安装和初始化 17 | 18 | ```typescript | pure 19 | import Skeleton from '@ant-design/pro-skeleton'; 20 | 21 | return ; 22 | ``` 23 | 24 | ## DEMO 25 | 26 | ### List 27 | 28 | 29 | 30 | ### 结果页 31 | 32 | 33 | 34 | ### 详情页 35 | 36 | 37 | 38 | ## API 39 | 40 | | 参数 | 说明 | 类型 | 默认值 | 41 | | --- | --- | --- | --- | 42 | | type | 不同类型的骨架屏 | `'list' \| 'result' \| 'descriptions'` | list | 43 | | active | 是否显示动态 | boolean | true | 44 | | pageHeader | 是否显示 pageHeader 的骨架屏 descriptions 和 list 有效 | - | - | 45 | | statistic | 统计信息骨架屏的数量 | `number` \| `false` | - | 46 | | list | 列表的骨架屏,可以控制数量 | `number` \| `false` | - | 47 | | toolbar | 列表的操作栏骨架屏 | boolean | - | 48 | | renderFormItem | 自定义 `mode=update 或 edit` 下的 dom 表现,一般用于渲染编辑框 | - | - | 49 | | render | 自定义 `mode=read` 下的 dom 表现,只是单纯的表现形式 | - | - | 50 | -------------------------------------------------------------------------------- /packages/utils/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXujiang/best-cps/16bc24f0b314ee3ef0a4ea4aef1e4028b862908d/packages/utils/CHANGELOG.md -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # @ant-design/pro-utils 2 | 3 | > @ant-design/pro-utils. 4 | 5 | See our website [@ant-design/pro-utils](https://procomponent.ant.design/) for more information. 6 | 7 | ## Install 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install --save @ant-design/pro-utils 13 | ``` 14 | 15 | or using yarn: 16 | 17 | ```bash 18 | $ yarn add @ant-design/pro-utils 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xu-utils", 3 | "version": "1.24.7", 4 | "description": "xu-utils工具包", 5 | "keywords": [ 6 | "javascript", 7 | "js tool" 8 | ], 9 | "sideEffects": [ 10 | "*.less" 11 | ], 12 | "homepage": "https://github.com/MrXujiang/best-cps/tree/master/packages/utils#readme", 13 | "bugs": "https://github.com/MrXujiang/best-cps/issues", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/MrXujiang/best-cps" 17 | }, 18 | "license": "MIT", 19 | "main": "lib/index.js", 20 | "module": "es/index.js", 21 | "types": "lib/index.d.ts", 22 | "files": [ 23 | "lib", 24 | "dist", 25 | "es" 26 | ], 27 | "browserslist": [ 28 | "last 2 versions", 29 | "Firefox ESR", 30 | "> 1%", 31 | "ie >= 11" 32 | ], 33 | "dependencies": { 34 | "@ant-design/icons": "^4.3.0", 35 | "@ant-design/pro-provider": "1.4.19", 36 | "classnames": "^2.2.6", 37 | "lodash.merge": "^4.6.2", 38 | "moment": "^2.27.0", 39 | "rc-util": "^5.0.6", 40 | "react-sortable-hoc": "^2.0.0", 41 | "swr": "^1.1.0-beta.0" 42 | }, 43 | "peerDependencies": { 44 | "antd": "4.x", 45 | "react": ">=16.9.0", 46 | "react-dom": ">=16.9.0" 47 | }, 48 | "publishConfig": { 49 | "access": "public" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/utils/src/array-move/index.ts: -------------------------------------------------------------------------------- 1 | export function arrayMoveMutable( 2 | array: ValueType[], 3 | fromIndex: number, 4 | toIndex: number, 5 | ) { 6 | const startIndex = fromIndex < 0 ? array.length + fromIndex : fromIndex; 7 | 8 | if (startIndex >= 0 && startIndex < array.length) { 9 | const endIndex = toIndex < 0 ? array.length + toIndex : toIndex; 10 | const [item] = array.splice(fromIndex, 1); 11 | array.splice(endIndex, 0, item); 12 | } 13 | } 14 | 15 | export function arrayMoveImmutable(array: T[], fromIndex: number, toIndex: number) { 16 | const newArray = [...array]; 17 | arrayMoveMutable(newArray, fromIndex, toIndex); 18 | return newArray; 19 | } 20 | -------------------------------------------------------------------------------- /packages/utils/src/conversionMomentValue/index.ts: -------------------------------------------------------------------------------- 1 | import type { InternalNamePath, NamePath } from 'antd/lib/form/interface'; 2 | import moment from 'moment'; 3 | import get from 'rc-util/lib/utils/get'; 4 | import isNil from '../isNil'; 5 | import type { ProFieldValueType } from '../typing'; 6 | 7 | type DateFormatter = 'number' | 'string' | false; 8 | 9 | export const dateFormatterMap = { 10 | time: 'HH:mm:ss', 11 | timeRange: 'HH:mm:ss', 12 | date: 'YYYY-MM-DD', 13 | dateWeek: 'YYYY-wo', 14 | dateMonth: 'YYYY-MM', 15 | dateQuarter: 'YYYY-QQ', 16 | dateYear: 'YYYY', 17 | dateRange: 'YYYY-MM-DD', 18 | dateTime: 'YYYY-MM-DD HH:mm:ss', 19 | dateTimeRange: 'YYYY-MM-DD HH:mm:ss', 20 | }; 21 | 22 | function isObject(o: any) { 23 | return Object.prototype.toString.call(o) === '[object Object]'; 24 | } 25 | 26 | export function isPlainObject(o: { constructor: any }) { 27 | if (isObject(o) === false) return false; 28 | 29 | // If has modified constructor 30 | const ctor = o.constructor; 31 | if (ctor === undefined) return true; 32 | 33 | // If has modified prototype 34 | const prot = ctor.prototype; 35 | if (isObject(prot) === false) return false; 36 | 37 | // If constructor does not have an Object-specific method 38 | if (prot.hasOwnProperty('isPrototypeOf') === false) { 39 | return false; 40 | } 41 | 42 | // Most likely a plain Object 43 | return true; 44 | } 45 | 46 | /** 47 | * 根据不同的格式转化 moment 48 | * 49 | * @param value 50 | * @param dateFormatter 51 | * @param valueType 52 | */ 53 | const convertMoment = (value: moment.Moment, dateFormatter: string | false, valueType: string) => { 54 | if (!dateFormatter) { 55 | return value; 56 | } 57 | if (moment.isMoment(value)) { 58 | if (dateFormatter === 'number') { 59 | return value.valueOf(); 60 | } 61 | if (dateFormatter === 'string') { 62 | return value.format(dateFormatterMap[valueType] || 'YYYY-MM-DD HH:mm:ss'); 63 | } 64 | if (typeof dateFormatter === 'string' && dateFormatter !== 'string') { 65 | return value.format(dateFormatter); 66 | } 67 | } 68 | return value; 69 | }; 70 | 71 | /** 72 | * 这里主要是来转化一下数据 将 moment 转化为 string 将 all 默认删除 73 | * 74 | * @param value 75 | * @param dateFormatter 76 | * @param proColumnsMap 77 | */ 78 | const conversionMomentValue = ( 79 | value: T, 80 | dateFormatter: DateFormatter, 81 | valueTypeMap: Record< 82 | string, 83 | | { 84 | valueType: ProFieldValueType; 85 | dateFormat: string; 86 | } 87 | | any 88 | >, 89 | omitNil?: boolean, 90 | parentKey?: NamePath, 91 | ): T => { 92 | const tmpValue = {} as T; 93 | // 如果 value 是 string | null | Blob类型 其中之一,直接返回 94 | // 形如 {key: [File, File]} 的表单字段当进行第二次递归时会导致其直接越过 typeof value !== 'object' 这一判断 https://github.com/ant-design/pro-components/issues/2071 95 | if (typeof value !== 'object' || isNil(value) || value instanceof Blob || Array.isArray(value)) { 96 | return value; 97 | } 98 | Object.keys(value).forEach((key) => { 99 | const namePath: InternalNamePath = parentKey ? ([parentKey, key].flat(1) as string[]) : [key]; 100 | const valueFormatMap = get(valueTypeMap, namePath) || 'text'; 101 | 102 | let valueType: ProFieldValueType = 'text'; 103 | let dateFormat: string | undefined; 104 | if (typeof valueFormatMap === 'string') { 105 | valueType = valueFormatMap as ProFieldValueType; 106 | } else if (valueFormatMap) { 107 | valueType = valueFormatMap.valueType; 108 | dateFormat = valueFormatMap.dateFormat; 109 | } 110 | const itemValue = value[key]; 111 | if (isNil(itemValue) && omitNil) { 112 | return; 113 | } 114 | // 处理嵌套的情况 115 | if ( 116 | isPlainObject(itemValue) && 117 | // 不是数组 118 | !Array.isArray(itemValue) && 119 | // 不是 moment 120 | !moment.isMoment(itemValue) 121 | ) { 122 | tmpValue[key] = conversionMomentValue(itemValue, dateFormatter, valueTypeMap, omitNil, [key]); 123 | return; 124 | } 125 | // 处理 FormList 的 value 126 | if (Array.isArray(itemValue)) { 127 | tmpValue[key] = itemValue.map((arrayValue, index) => { 128 | if (moment.isMoment(arrayValue)) { 129 | return convertMoment(arrayValue, dateFormat || dateFormatter, valueType); 130 | } 131 | return conversionMomentValue(arrayValue, dateFormatter, valueTypeMap, omitNil, [ 132 | key, 133 | `${index}`, 134 | ]); 135 | }); 136 | return; 137 | } 138 | tmpValue[key] = convertMoment(itemValue, dateFormat || dateFormatter, valueType); 139 | }); 140 | 141 | return tmpValue; 142 | }; 143 | 144 | export default conversionMomentValue; 145 | -------------------------------------------------------------------------------- /packages/utils/src/dateArrayFormatter/index.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | /** 4 | * 格式化区域日期 5 | * 6 | * @param value 7 | */ 8 | const dateArrayFormatter = (value: any[], format: string) => { 9 | const [startText, endText] = Array.isArray(value) ? value : []; 10 | // activePickerIndex for https://github.com/ant-design/ant-design/issues/22158 11 | const parsedStartText: string = startText ? moment(startText).format(format) : ''; 12 | const parsedEndText: string = endText ? moment(endText).format(format) : ''; 13 | const valueStr: string = 14 | parsedStartText && parsedEndText && `${parsedStartText} ~ ${parsedEndText}`; 15 | 16 | return valueStr; 17 | }; 18 | 19 | export default dateArrayFormatter; 20 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useDebounceFn/index.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import { useEffect, useRef, useCallback } from 'react'; 3 | 4 | export type ReturnValue = { 5 | run: (...args: T) => void; 6 | cancel: () => void; 7 | }; 8 | const useUpdateEffect: typeof useEffect = (effect, deps) => { 9 | const isMounted = useRef(false); 10 | 11 | useEffect(() => { 12 | if (!isMounted.current) { 13 | isMounted.current = true; 14 | } else { 15 | return effect(); 16 | } 17 | return () => undefined; 18 | }, deps); 19 | }; 20 | 21 | function useDebounceFn( 22 | fn: (...args: T) => Promise, 23 | deps: DependencyList | number, 24 | wait?: number, 25 | ): ReturnValue { 26 | // eslint-disable-next-line no-underscore-dangle 27 | const hooksDeps: DependencyList = (Array.isArray(deps) ? deps : []) as DependencyList; 28 | // eslint-disable-next-line no-underscore-dangle 29 | const hookWait: number = typeof deps === 'number' ? deps : wait || 0; 30 | const timer = useRef(); 31 | 32 | const fnRef = useRef(fn); 33 | fnRef.current = fn; 34 | 35 | const cancel = useCallback(() => { 36 | if (timer.current) { 37 | clearTimeout(timer.current); 38 | } 39 | }, []); 40 | 41 | const run = useCallback( 42 | async (...args: any): Promise => { 43 | return new Promise((resolve) => { 44 | cancel(); 45 | timer.current = setTimeout(async () => { 46 | await fnRef.current(...args); 47 | resolve(); 48 | }, hookWait); 49 | }); 50 | }, 51 | [hookWait, cancel], 52 | ); 53 | 54 | useUpdateEffect(() => { 55 | run(); 56 | return cancel; 57 | }, [...hooksDeps, run]); 58 | 59 | useEffect(() => cancel, []); 60 | 61 | return { 62 | run, 63 | cancel, 64 | }; 65 | } 66 | 67 | export default useDebounceFn; 68 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useDeepCompareEffect/index.ts: -------------------------------------------------------------------------------- 1 | import type { DependencyList } from 'react'; 2 | import { useEffect, useRef } from 'react'; 3 | import isDeepEqualReact from '../../isDeepEqualReact'; 4 | 5 | export const isDeepEqual: (a: any, b: any) => boolean = isDeepEqualReact; 6 | 7 | function useDeepCompareMemoize(value: any) { 8 | const ref = useRef(); 9 | // it can be done by using useMemo as well 10 | // but useRef is rather cleaner and easier 11 | if (!isDeepEqual(value, ref.current)) { 12 | ref.current = value; 13 | } 14 | 15 | return ref.current; 16 | } 17 | 18 | function useDeepCompareEffect(effect: React.EffectCallback, dependencies: DependencyList = []) { 19 | useEffect(effect, useDeepCompareMemoize(dependencies)); 20 | } 21 | 22 | export default useDeepCompareEffect; 23 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useDocumentTitle/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import isBrowser from '../../isBrowser'; 3 | 4 | function useDocumentTitle( 5 | titleInfo: { 6 | title: string; 7 | id: string; 8 | pageName: string; 9 | }, 10 | appDefaultTitle: string | false, 11 | ) { 12 | const titleText = typeof titleInfo.pageName === 'string' ? titleInfo.title : appDefaultTitle; 13 | useEffect(() => { 14 | if (isBrowser() && titleText) { 15 | document.title = titleText; 16 | } 17 | }, [titleInfo.title]); 18 | } 19 | 20 | export default useDocumentTitle; 21 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/useFetchData/index.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef, useMemo } from 'react'; 2 | import useSWR, { mutate } from 'swr'; 3 | 4 | let testId = 0; 5 | 6 | export type ProRequestData> = (params: U, props: any) => Promise; 7 | 8 | function useFetchData = Record>(props: { 9 | proFieldKey?: React.Key; 10 | params?: U; 11 | request?: ProRequestData; 12 | }): [T, () => void] { 13 | /** Key 是用来缓存请求的,如果不在是有问题 */ 14 | const [cacheKey] = useState(() => { 15 | if (props.proFieldKey) { 16 | return props.proFieldKey.toString(); 17 | } 18 | testId += 1; 19 | return testId.toString(); 20 | }); 21 | 22 | const proFieldKeyRef = useRef(cacheKey); 23 | 24 | const fetchData = async () => { 25 | const loadData = await props.request?.(props.params as U, props); 26 | return loadData; 27 | }; 28 | 29 | const key = useMemo(() => { 30 | if (!props.params) { 31 | return proFieldKeyRef.current; 32 | } 33 | return [proFieldKeyRef.current, JSON.stringify(props.params)]; 34 | }, [props.params]); 35 | 36 | const { data, error } = useSWR(key, fetchData, { 37 | revalidateOnFocus: false, 38 | shouldRetryOnError: false, 39 | revalidateOnReconnect: false, 40 | }); 41 | 42 | return [ 43 | (data as T) || error, 44 | () => { 45 | mutate(key); 46 | }, 47 | ]; 48 | } 49 | 50 | export default useFetchData; 51 | -------------------------------------------------------------------------------- /packages/utils/src/hooks/usePrevious/index.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | const usePrevious = (state: T): T | undefined => { 4 | const ref = useRef(); 5 | 6 | useEffect(() => { 7 | ref.current = state; 8 | }); 9 | 10 | return ref.current; 11 | }; 12 | 13 | export default usePrevious; 14 | -------------------------------------------------------------------------------- /packages/utils/src/index.tsx: -------------------------------------------------------------------------------- 1 | import isBrowser from './isBrowser'; 2 | import isImg from './isImg'; 3 | import isUrl from './isUrl'; 4 | import isNil from './isNil'; 5 | import isDropdownValueType from './isDropdownValueType'; 6 | import omitUndefined from './omitUndefined'; 7 | import omitBoolean from './omitBoolean'; 8 | import omitUndefinedAndEmptyArr from './omitUndefinedAndEmptyArr'; 9 | import useMountMergeState from './useMountMergeState'; 10 | 11 | /** Hooks */ 12 | import useDebounceFn from './hooks/useDebounceFn'; 13 | import usePrevious from './hooks/usePrevious'; 14 | import conversionMomentValue, { dateFormatterMap } from './conversionMomentValue'; 15 | import transformKeySubmitValue from './transformKeySubmitValue'; 16 | import parseValueToMoment from './parseValueToMoment'; 17 | import useDeepCompareEffect from './hooks/useDeepCompareEffect'; 18 | import useDocumentTitle from './hooks/useDocumentTitle'; 19 | import type { ProRequestData } from './hooks/useFetchData'; 20 | import useFetchData from './hooks/useFetchData'; 21 | 22 | /** Type */ 23 | import type { 24 | ProSchema, 25 | ProSchemaValueEnumMap, 26 | ProSchemaValueEnumObj, 27 | ProSchemaComponentTypes, 28 | ProCoreActionType, 29 | SearchTransformKeyFn, 30 | ProTableEditableFnType, 31 | ProFieldValueType, 32 | ProFieldValueEnumType, 33 | ProFieldRequestData, 34 | ProFieldValueObjectType, 35 | ProFieldTextType, 36 | RequestOptionsType, 37 | ProFieldProps, 38 | } from './typing'; 39 | import { runFunction } from './runFunction'; 40 | import type { 41 | BaseProFieldFC, 42 | ProFieldFCMode, 43 | ProFieldFCRenderProps, 44 | ProRenderFieldPropsType, 45 | } from '@ant-design/pro-provider'; 46 | import dateArrayFormatter from './dateArrayFormatter'; 47 | import isDeepEqualReact from './isDeepEqualReact'; 48 | import { arrayMoveImmutable } from './array-move'; 49 | 50 | export type { 51 | RequestOptionsType, 52 | ProSchema, 53 | ProCoreActionType, 54 | ProSchemaComponentTypes, 55 | ProSchemaValueEnumMap, 56 | ProSchemaValueEnumObj, 57 | SearchTransformKeyFn, 58 | ProTableEditableFnType, 59 | ProRequestData, 60 | ProFieldRequestData, 61 | ProFieldValueType, 62 | ProRenderFieldPropsType, 63 | ProFieldFCRenderProps, 64 | ProFieldFCMode, 65 | BaseProFieldFC, 66 | ProFieldTextType, 67 | ProFieldValueEnumType, 68 | ProFieldValueObjectType, 69 | ProFieldProps, 70 | }; 71 | 72 | export { 73 | isDeepEqualReact, 74 | arrayMoveImmutable, 75 | dateFormatterMap, 76 | // function 77 | transformKeySubmitValue, 78 | conversionMomentValue as conversionSubmitValue, 79 | conversionMomentValue, 80 | parseValueToMoment, 81 | useDocumentTitle, 82 | isImg, 83 | omitBoolean, 84 | isNil, 85 | isDropdownValueType, 86 | omitUndefined, 87 | omitUndefinedAndEmptyArr, 88 | isUrl, 89 | isBrowser, 90 | runFunction, 91 | dateArrayFormatter, 92 | // hooks 93 | useDeepCompareEffect, 94 | usePrevious, 95 | useDebounceFn, 96 | useMountMergeState, 97 | useFetchData, 98 | }; 99 | -------------------------------------------------------------------------------- /packages/utils/src/isBrowser/index.ts: -------------------------------------------------------------------------------- 1 | const isNode = 2 | typeof process !== 'undefined' && process.versions != null && process.versions.node != null; 3 | 4 | const isBrowser = () => { 5 | if (process.env.NODE_ENV === 'TEST') { 6 | return true; 7 | } 8 | return typeof window !== 'undefined' && typeof window.document !== 'undefined' && !isNode; 9 | }; 10 | 11 | export default isBrowser; 12 | -------------------------------------------------------------------------------- /packages/utils/src/isDeepEqualReact/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | /* eslint-disable no-continue */ 3 | /* eslint-disable @typescript-eslint/no-unused-vars */ 4 | /* eslint-disable no-self-compare */ 5 | /* eslint-disable eqeqeq */ 6 | /* eslint-disable no-plusplus */ 7 | // do not edit .js files directly - edit src/index.jst 8 | 9 | function isDeepEqualReact(a: any, b: any) { 10 | if (a === b) return true; 11 | 12 | if (a && b && typeof a === 'object' && typeof b === 'object') { 13 | if (a.constructor !== b.constructor) return false; 14 | 15 | let length; 16 | let i; 17 | let keys; 18 | if (Array.isArray(a)) { 19 | length = a.length; 20 | if (length != b.length) return false; 21 | for (i = length; i-- !== 0; ) if (!isDeepEqualReact(a[i], b[i])) return false; 22 | return true; 23 | } 24 | 25 | if (a instanceof Map && b instanceof Map) { 26 | if (a.size !== b.size) return false; 27 | for (i of a.entries()) if (!b.has(i[0])) return false; 28 | for (i of a.entries()) if (!isDeepEqualReact(i[1], b.get(i[0]))) return false; 29 | return true; 30 | } 31 | 32 | if (a instanceof Set && b instanceof Set) { 33 | if (a.size !== b.size) return false; 34 | for (i of a.entries()) if (!b.has(i[0])) return false; 35 | return true; 36 | } 37 | 38 | if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { 39 | // @ts-ignore 40 | length = a.length; 41 | // @ts-ignore 42 | if (length != b.length) return false; 43 | for (i = length; i-- !== 0; ) if (a[i] !== b[i]) return false; 44 | return true; 45 | } 46 | 47 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; 48 | if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); 49 | if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); 50 | 51 | // eslint-disable-next-line prefer-const 52 | keys = Object.keys(a); 53 | length = keys.length; 54 | if (length !== Object.keys(b).length) return false; 55 | 56 | for (i = length; i-- !== 0; ) 57 | if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; 58 | 59 | for (i = length; i-- !== 0; ) { 60 | const key = keys[i]; 61 | 62 | if (key === '_owner' && a.$$typeof) { 63 | // React-specific: avoid traversing React elements' _owner. 64 | // _owner contains circular references 65 | // and is not needed when comparing the actual elements (and not their owners) 66 | continue; 67 | } 68 | 69 | if (!isDeepEqualReact(a[key], b[key])) return false; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | // true if both NaN, false otherwise 76 | return a !== a && b !== b; 77 | } 78 | 79 | export default isDeepEqualReact; 80 | -------------------------------------------------------------------------------- /packages/utils/src/isDropdownValueType/index.ts: -------------------------------------------------------------------------------- 1 | const isDropdownValueType = (valueType: string) => { 2 | let isDropdown = false; 3 | if ( 4 | (typeof valueType === 'string' && 5 | valueType.startsWith('date') && 6 | !valueType.endsWith('Range')) || 7 | valueType === 'select' 8 | ) { 9 | isDropdown = true; 10 | } 11 | return isDropdown; 12 | }; 13 | 14 | export default isDropdownValueType; 15 | -------------------------------------------------------------------------------- /packages/utils/src/isImg/index.ts: -------------------------------------------------------------------------------- 1 | /** 判断是否是图片链接 */ 2 | function isImg(path: string): boolean { 3 | return /\w.(png|jpg|jpeg|svg|webp|gif|bmp)$/i.test(path); 4 | } 5 | 6 | export default isImg; 7 | -------------------------------------------------------------------------------- /packages/utils/src/isNil/index.ts: -------------------------------------------------------------------------------- 1 | const isNil = (value: any) => value === null || value === undefined; 2 | 3 | export default isNil; 4 | -------------------------------------------------------------------------------- /packages/utils/src/isUrl/index.ts: -------------------------------------------------------------------------------- 1 | const isUrl = (path: string): boolean => { 2 | if (!path.startsWith('http')) { 3 | return false; 4 | } 5 | try { 6 | const url = new URL(path); 7 | return !!url; 8 | } catch (error) { 9 | return false; 10 | } 11 | }; 12 | 13 | export default isUrl; 14 | -------------------------------------------------------------------------------- /packages/utils/src/merge/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-rest-params */ 2 | const merge = (...rest: any[]): T => { 3 | const obj = {}; 4 | const il = rest.length; 5 | let key; 6 | let i = 0; 7 | for (; i < il; i += 1) { 8 | // eslint-disable-next-line no-restricted-syntax 9 | for (key in rest[i]) { 10 | if (rest[i].hasOwnProperty(key)) { 11 | obj[key] = rest[i][key]; 12 | } 13 | } 14 | } 15 | return obj as T; 16 | }; 17 | 18 | export { merge }; 19 | -------------------------------------------------------------------------------- /packages/utils/src/omitBoolean/index.ts: -------------------------------------------------------------------------------- 1 | const omitBoolean = (obj: boolean | T): T | undefined => { 2 | if (obj && obj !== true) { 3 | return obj; 4 | } 5 | return undefined; 6 | }; 7 | 8 | export default omitBoolean; 9 | -------------------------------------------------------------------------------- /packages/utils/src/omitUndefined/index.ts: -------------------------------------------------------------------------------- 1 | const omitUndefined = (obj: T): T => { 2 | const newObj = {} as T; 3 | Object.keys(obj || {}).forEach((key) => { 4 | if (obj[key] !== undefined) { 5 | newObj[key] = obj[key]; 6 | } 7 | }); 8 | if (Object.keys(newObj).length < 1) { 9 | return undefined as any; 10 | } 11 | return newObj; 12 | }; 13 | 14 | export default omitUndefined; 15 | -------------------------------------------------------------------------------- /packages/utils/src/omitUndefinedAndEmptyArr/index.ts: -------------------------------------------------------------------------------- 1 | const omitUndefinedAndEmptyArr = (obj: T): T => { 2 | const newObj = {} as T; 3 | Object.keys(obj || {}).forEach((key) => { 4 | if (Array.isArray(obj[key]) && obj[key]?.length === 0) { 5 | return; 6 | } 7 | if (obj[key] === undefined) { 8 | return; 9 | } 10 | newObj[key] = obj[key]; 11 | }); 12 | return newObj; 13 | }; 14 | 15 | export default omitUndefinedAndEmptyArr; 16 | -------------------------------------------------------------------------------- /packages/utils/src/parseValueToMoment/index.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import isNil from '../isNil'; 3 | 4 | type DateValue = moment.Moment | moment.Moment[] | string | string[] | number | number[]; 5 | 6 | const parseValueToMoment = ( 7 | value: DateValue, 8 | formatter?: string, 9 | ): moment.Moment | moment.Moment[] | null | undefined => { 10 | if (isNil(value) || moment.isMoment(value)) { 11 | return value as moment.Moment | null | undefined; 12 | } 13 | if (Array.isArray(value)) { 14 | return (value as any[]).map((v) => parseValueToMoment(v, formatter) as moment.Moment); 15 | } 16 | return moment(value, formatter); 17 | }; 18 | 19 | export default parseValueToMoment; 20 | -------------------------------------------------------------------------------- /packages/utils/src/runFunction/index.ts: -------------------------------------------------------------------------------- 1 | /** 如果是个方法执行一下它 */ 2 | export function runFunction(valueEnum: any, ...rest: T) { 3 | if (typeof valueEnum === 'function') { 4 | return valueEnum(...rest); 5 | } 6 | return valueEnum; 7 | } 8 | -------------------------------------------------------------------------------- /packages/utils/src/transformKeySubmitValue/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type { SearchTransformKeyFn } from '../typing'; 3 | import get from 'rc-util/lib/utils/get'; 4 | import namePathSet from 'rc-util/lib/utils/set'; 5 | import merge from 'lodash.merge'; 6 | import isNil from '../isNil'; 7 | 8 | export type DataFormatMapType = Record; 9 | 10 | const transformKeySubmitValue = ( 11 | values: T, 12 | dataFormatMapRaw: Record, 13 | omit: boolean = true, 14 | ) => { 15 | // ignore nil transform 16 | const dataFormatMap = Object.keys(dataFormatMapRaw).reduce((ret, key) => { 17 | const value = dataFormatMapRaw[key]; 18 | if (!isNil(value)) { 19 | // eslint-disable-next-line no-param-reassign 20 | ret[key] = value! as SearchTransformKeyFn; // can't be undefined 21 | } 22 | return ret; 23 | }, {} as Record); 24 | 25 | if (Object.keys(dataFormatMap).length < 1) { 26 | return values; 27 | } 28 | // 如果 value 是 string | null | Blob类型 其中之一,直接返回 29 | // 形如 {key: [File, File]} 的表单字段当进行第二次递归时会导致其直接越过 typeof value !== 'object' 这一判断 https://github.com/ant-design/pro-components/issues/2071 30 | if (typeof values !== 'object' || isNil(values) || values instanceof Blob) { 31 | return values; 32 | } 33 | let finalValues = {} as T; 34 | 35 | const gen = (tempValues: T, parentsKey?: React.Key[]) => { 36 | let result = {} as T; 37 | 38 | if (tempValues == null || tempValues === undefined) { 39 | return result; 40 | } 41 | 42 | Object.keys(tempValues).forEach((entityKey) => { 43 | const key = parentsKey ? [parentsKey, entityKey].flat(1) : [entityKey].flat(1); 44 | const itemValue = tempValues[entityKey]; 45 | const transformFunction = get(dataFormatMap, key); 46 | const transform = () => { 47 | const tempKey = 48 | typeof transformFunction === 'function' 49 | ? transformFunction?.(itemValue, entityKey, tempValues) 50 | : entityKey; 51 | // { [key:string]:any } 数组也能通过编译 52 | if (Array.isArray(tempKey)) { 53 | result = namePathSet(result, tempKey, itemValue); 54 | return; 55 | } 56 | if (typeof tempKey === 'object') { 57 | finalValues = { 58 | ...finalValues, 59 | ...tempKey, 60 | }; 61 | } else if (tempKey) { 62 | result = namePathSet(result, [tempKey], itemValue); 63 | } 64 | }; 65 | 66 | /** 如果存在转化器提前渲染一下 */ 67 | if (transformFunction && typeof transformFunction === 'function') { 68 | transform(); 69 | } 70 | 71 | if ( 72 | typeof itemValue === 'object' && 73 | !Array.isArray(itemValue) && 74 | !React.isValidElement(itemValue) && // ignore walk throungh React Element 75 | !(itemValue instanceof Blob) // ignore walk throungh Blob 76 | ) { 77 | const genValues = gen(itemValue, key); 78 | if (Object.keys(genValues).length < 1) { 79 | return; 80 | } 81 | result = namePathSet(result, [entityKey], genValues); 82 | return; 83 | } 84 | transform(); 85 | }); 86 | // namePath、transform在omit为false时需正常返回 https://github.com/ant-design/pro-components/issues/2901#issue-908097115 87 | return omit ? result : tempValues; 88 | }; 89 | 90 | finalValues = merge({}, gen(values), finalValues); 91 | 92 | return finalValues; 93 | }; 94 | 95 | export default transformKeySubmitValue; 96 | -------------------------------------------------------------------------------- /packages/utils/src/useMountMergeState/index.ts: -------------------------------------------------------------------------------- 1 | import useMergedState from 'rc-util/lib/hooks/useMergedState'; 2 | import { useEffect, useRef } from 'react'; 3 | 4 | type Dispatch = (value: A) => void; 5 | 6 | function useMountMergeState( 7 | initialState: S | (() => S), 8 | option?: { 9 | defaultValue?: S; 10 | value?: S; 11 | onChange?: (value: S, prevValue: S) => void; 12 | postState?: (value: S) => S; 13 | }, 14 | ): [S, Dispatch] { 15 | const mountRef = useRef(false); 16 | const frame = useRef(0); 17 | 18 | useEffect(() => { 19 | mountRef.current = true; 20 | return () => { 21 | mountRef.current = false; 22 | }; 23 | }); 24 | 25 | const [state, setState] = useMergedState(initialState, option); 26 | const mountSetState: Dispatch = (prevState: S) => { 27 | cancelAnimationFrame(frame.current); 28 | 29 | frame.current = requestAnimationFrame(() => { 30 | if (mountRef.current) { 31 | setState(prevState); 32 | } 33 | }); 34 | }; 35 | return [state, mountSetState]; 36 | } 37 | 38 | export default useMountMergeState; 39 | -------------------------------------------------------------------------------- /packages/x6-react/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXujiang/best-cps/16bc24f0b314ee3ef0a4ea4aef1e4028b862908d/packages/x6-react/CHANGELOG.md -------------------------------------------------------------------------------- /packages/x6-react/README.md: -------------------------------------------------------------------------------- 1 | # x6-react 2 | 3 | > x6-react. 4 | 5 | See our website [@ant-design/pro-skeleton](https://github.com/MrXujiang/best-cps) for more information. 6 | 7 | ## Install 8 | 9 | Using npm: 10 | 11 | ```bash 12 | $ npm install --save xu-skeleton 13 | ``` 14 | 15 | or using yarn: 16 | 17 | ```bash 18 | $ yarn add xu-skeleton 19 | ``` 20 | -------------------------------------------------------------------------------- /packages/x6-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xu-x6-react", 3 | "version": "1.0.2", 4 | "description": "x6-react", 5 | "keywords": [ 6 | "antd", 7 | "react-component", 8 | "x6", 9 | "x6-react" 10 | ], 11 | "sideEffects": [ 12 | "*.less" 13 | ], 14 | "homepage": "https://github.com/MrXujiang/best-cps/tree/master/packages/x6-react#readme", 15 | "bugs": "https://github.com/MrXujiang/best-cps/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/MrXujiang/best-cps" 19 | }, 20 | "license": "MIT", 21 | "main": "lib/index.js", 22 | "module": "es/index.js", 23 | "types": "lib/index.d.ts", 24 | "files": [ 25 | "lib", 26 | "es", 27 | "dist" 28 | ], 29 | "browserslist": [ 30 | "last 2 versions", 31 | "Firefox ESR", 32 | "> 1%", 33 | "ie >= 11" 34 | ], 35 | "dependencies": { 36 | "@antv/x6": "^1.28.0", 37 | "use-media-antd-query": "^1.0.6" 38 | }, 39 | "peerDependencies": { 40 | "antd": "4.x", 41 | "react": ">=16.9.0" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/x6-react/src/component/Base/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from 'antd'; 3 | import { Graph, DataUri, Shape } from '@antv/x6'; 4 | 5 | const data = { 6 | // 节点 7 | nodes: [ 8 | { 9 | id: 'node1', // String,可选,节点的唯一标识 10 | x: 40, // Number,必选,节点位置的 x 值 11 | y: 40, // Number,必选,节点位置的 y 值 12 | // rx: 5, 13 | // ry: 5, 14 | width: 80, // Number,可选,节点大小的 width 值 15 | height: 40, // Number,可选,节点大小的 height 值 16 | label: 'hello', // String,节点标签 17 | }, 18 | { 19 | id: 'node2', // String,节点的唯一标识 20 | x: 160, // Number,必选,节点位置的 x 值 21 | y: 180, // Number,必选,节点位置的 y 值 22 | width: 80, // Number,可选,节点大小的 width 值 23 | height: 40, // Number,可选,节点大小的 height 值 24 | label: 'world', // String,节点标签 25 | shape: 'ellipse' 26 | }, 27 | ], 28 | // 边 29 | edges: [ 30 | { 31 | source: 'node1', // String,必须,起始节点 id 32 | target: 'node2', // String,必须,目标节点 id 33 | connector: 'rounded', 34 | vertices: [ 35 | { x: 100, y: 200 }, 36 | { x: 300, y: 120 }, 37 | ], 38 | router: 'manhattan', 39 | label: 'dooring' 40 | }, 41 | ], 42 | }; 43 | 44 | interface IState { 45 | scale: number 46 | translate: [number, number] 47 | } 48 | 49 | class GraphComponent extends React.Component<{}, IState> { 50 | constructor(props: {}) { 51 | super(props); 52 | this.state = { 53 | scale: 1, 54 | translate: [0, 0] 55 | } 56 | } 57 | 58 | graphRef = React.createRef() 59 | 60 | createRect = (id: string, w: number, h: number, label: string) => { 61 | const rect = new Shape.Rect({ 62 | id, 63 | x: 40, 64 | y: 40, 65 | width: w, 66 | height: h, 67 | label, 68 | zIndex: 2, 69 | }) 70 | this.graphRef.current.addNode(rect) 71 | } 72 | 73 | handleScale = () => { 74 | this.setState(prev => { 75 | const curScale = prev.scale 76 | // 缩放是递增的 77 | this.graphRef.current.zoom(0.1) 78 | return { 79 | scale: curScale 80 | } 81 | }) 82 | } 83 | 84 | handleTranslate = () => { 85 | this.setState(prev => { 86 | const curTransform = prev.translate.map(v => v + 10) as [number, number] 87 | const [x, y] = curTransform 88 | this.graphRef.current.translate(x, y) 89 | return { 90 | translate: curTransform 91 | } 92 | }) 93 | } 94 | 95 | handleToSvg = () => { 96 | this.graphRef.current.toSVG((dataUri: string) => { 97 | // 下载 98 | DataUri.downloadDataUri(DataUri.svgToDataUrl(dataUri), 'chart.svg') 99 | }) 100 | } 101 | 102 | handleDispose = () => { 103 | this.graphRef.current.dispose() 104 | } 105 | 106 | render() { 107 | return
108 |
109 | 110 | 111 | 112 | 113 | 114 |
115 | } 116 | componentDidMount() { 117 | this.graphRef.current = new Graph({ 118 | container: document.getElementById('container') as HTMLDivElement, 119 | width: 600, 120 | height: 400, 121 | grid: { 122 | size: 10, // 网格大小 10px 123 | visible: true, // 渲染网格背景 124 | }, 125 | panning: { 126 | enabled: true, 127 | eventTypes: ['leftMouseDown', 'mouseWheel'] 128 | }, 129 | }); 130 | this.graphRef.current.fromJSON(data) 131 | this.graphRef.current.centerContent() 132 | } 133 | } 134 | 135 | export default GraphComponent 136 | -------------------------------------------------------------------------------- /packages/x6-react/src/demos/base.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Graph } from 'xu-x6-react'; 3 | 4 | export default () => ( 5 |
11 | 12 |
13 | ); 14 | -------------------------------------------------------------------------------- /packages/x6-react/src/index.tsx: -------------------------------------------------------------------------------- 1 | // import React from 'react'; 2 | import Graph from './component/Base'; 3 | 4 | export { 5 | Graph 6 | }; 7 | 8 | // export default PageSkeleton; 9 | -------------------------------------------------------------------------------- /packages/x6-react/src/x6-react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: x6-react - 基于react的图编辑框架 3 | group: 4 | path: / 5 | nav: 6 | title: 组件 7 | path: /components 8 | --- 9 | 10 | ## 安装和初始化 11 | 12 | ```typescript | pure 13 | import { Graph } from 'xu-x6-react'; 14 | 15 | return ; 16 | ``` 17 | 18 | ## DEMO 19 | 20 | ### List 21 | 22 | 23 | 24 | 25 | ## API 26 | 27 | | 参数 | 说明 | 类型 | 默认值 | 28 | | --- | --- | --- | --- | 29 | | type | 不同类型的骨架屏 | `'list' \| 'result' \| 'descriptions'` | list | 30 | | active | 是否显示动态 | boolean | true | 31 | | pageHeader | 是否显示 pageHeader 的骨架屏 descriptions 和 list 有效 | - | - | 32 | | statistic | 统计信息骨架屏的数量 | `number` \| `false` | - | 33 | | list | 列表的骨架屏,可以控制数量 | `number` \| `false` | - | 34 | | toolbar | 列表的操作栏骨架屏 | boolean | - | 35 | | renderFormItem | 自定义 `mode=update 或 edit` 下的 dom 表现,一般用于渲染编辑框 | - | - | 36 | | render | 自定义 `mode=read` 下的 dom 表现,只是单纯的表现形式 | - | - | 37 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | procomponents.ant.design 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXujiang/best-cps/16bc24f0b314ee3ef0a4ea4aef1e4028b862908d/public/favicon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MrXujiang/best-cps/16bc24f0b314ee3ef0a4ea4aef1e4028b862908d/public/icon.png -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | // 自动生成组件库的package.json和readme文件 2 | const { existsSync, writeFileSync, readdirSync } = require('fs'); 3 | const { join } = require('path'); 4 | const { yParser } = require('@umijs/utils'); 5 | 6 | (async () => { 7 | const args = yParser(process.argv); 8 | const version = '1.0.0-beta.1'; 9 | 10 | const pkgs = readdirSync(join(__dirname, '../packages')).filter((pkg) => pkg.charAt(0) !== '.'); 11 | 12 | pkgs.forEach((shortName) => { 13 | const name = `min-pro-${shortName}`; 14 | 15 | const pkgJSONPath = join(__dirname, '..', 'packages', shortName, 'package.json'); 16 | const pkgJSONExists = existsSync(pkgJSONPath); 17 | let json; 18 | if (args.force || !pkgJSONExists) { 19 | json = { 20 | name, 21 | version, 22 | description: name, 23 | module: 'es/index.js', 24 | main: 'lib/index.js', 25 | types: 'lib/index.d.ts', 26 | files: ['lib', 'src', 'dist', 'es'], 27 | repository: { 28 | type: 'git', 29 | url: 'https://github.com/ant-design/pro-components', 30 | }, 31 | browserslist: ['last 2 versions', 'Firefox ESR', '> 1%', 'ie >= 11'], 32 | keywords: ['antd', 'admin', 'ant-design', 'ant-design-pro'], 33 | authors: [ 34 | 'chencheng (https://github.com/sorrycc)', 35 | 'chenshuai2144 (https://github.com/chenshuai2144)', 36 | ], 37 | license: 'MIT', 38 | bugs: 'http://github.com/umijs/plugins/issues', 39 | homepage: `https://github.com/ant-design/pro-components/tree/master/packages/${shortName}#readme`, 40 | peerDependencies: { 41 | umi: '3.x', 42 | }, 43 | publishConfig: { 44 | access: 'public', 45 | }, 46 | }; 47 | if (pkgJSONExists) { 48 | const pkg = require(pkgJSONPath); 49 | [ 50 | 'dependencies', 51 | 'devDependencies', 52 | 'peerDependencies', 53 | 'bin', 54 | 'version', 55 | 'files', 56 | 'authors', 57 | 'types', 58 | 'sideEffects', 59 | 'main', 60 | 'module', 61 | 'description', 62 | ].forEach((key) => { 63 | if (pkg[key]) json[key] = pkg[key]; 64 | }); 65 | } 66 | writeFileSync(pkgJSONPath, `${JSON.stringify(json, null, 2)}\n`); 67 | } 68 | 69 | const readmePath = join(__dirname, '..', 'packages', shortName, 'README.md'); 70 | if (args.force || !existsSync(readmePath)) { 71 | writeFileSync( 72 | readmePath, 73 | `# ${name} 74 | 75 | > ${json.description}. 76 | 77 | See our website [${name}](https://umijs.org/plugins/${shortName}) for more information. 78 | 79 | ## Install 80 | 81 | Using npm: 82 | 83 | \`\`\`bash 84 | $ npm install --save ${name} 85 | \`\`\` 86 | 87 | or using yarn: 88 | 89 | \`\`\`bash 90 | $ yarn add ${name} 91 | \`\`\` 92 | `, 93 | ); 94 | } 95 | }); 96 | })(); 97 | -------------------------------------------------------------------------------- /scripts/checkDeps.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | const parser = require('@babel/parser'); 3 | const traverse = require('@babel/traverse'); 4 | const t = require('babel-types'); 5 | const glob = require('glob'); 6 | const slash = require('slash'); 7 | const fs = require('fs'); 8 | const ora = require('ora'); 9 | const { join, posix } = require('path'); 10 | 11 | const spinner = ora(); 12 | 13 | const peerDependencies = ['antd', 'react', 'rc-field-form']; 14 | 15 | /** 16 | * 替换文件中的 formatMessage 17 | * 18 | * @param {any} ast 19 | */ 20 | const checkDepsByAst = (ast, filePath) => { 21 | return new Promise((resolve) => { 22 | traverse.default(ast, { 23 | enter(path) { 24 | if (path.isImportDeclaration()) { 25 | const importPath = path.node.source.value; 26 | 27 | if (!importPath) return; 28 | 29 | if (importPath.includes('/src')) { 30 | resolve({ 31 | success: false, 32 | message: 'import 不能包含 **/src/**', 33 | }); 34 | return; 35 | } 36 | 37 | if (importPath.startsWith('.')) { 38 | const importFile = slash(join(__dirname, '..', filePath, '..', importPath)); 39 | if (importFile.split('.').length > 1) { 40 | if (fs.existsSync(`${importFile}`)) return; 41 | resolve({ 42 | success: false, 43 | message: `${importFile} 路径错误,请检查大小写或路径错误`, 44 | }); 45 | return; 46 | } 47 | if ( 48 | !fs.existsSync(`${importFile}.ts`) && 49 | !fs.existsSync(`${importFile}.tsx`) && 50 | !fs.existsSync(`${importFile}/index.tsx`) && 51 | !fs.existsSync(`${importFile}/index.ts`) && 52 | !fs.existsSync(`${importFile}.d.ts`) 53 | ) { 54 | resolve({ 55 | success: false, 56 | message: `${importFile} 路径错误,请检查大小写或路径错误`, 57 | }); 58 | return; 59 | } 60 | } 61 | if (!importPath.startsWith('.') && path.node.importKind !== 'type') { 62 | const packagePath = slash(filePath.split(posix.sep).splice(0, 2).join(posix.sep)); 63 | try { 64 | // 检查包在不在 65 | require.resolve(importPath, { 66 | paths: [slash(join(__dirname, '..', packagePath))], 67 | }); 68 | if (peerDependencies.every((item) => !importPath.startsWith(item))) { 69 | const packageName = importPath.split(posix.sep)[0]; 70 | const packageJson = require(slash( 71 | join(__dirname, '..', packagePath, 'package.json'), 72 | )); 73 | if (!JSON.stringify(packageJson.dependencies).includes(packageName)) { 74 | resolve({ 75 | success: false, 76 | message: `${packagePath} 的 ${packageName} 依赖没有在 ${slash( 77 | join(__dirname, '..', packagePath, 'package.json'), 78 | )} 中申明`, 79 | }); 80 | return; 81 | } 82 | } 83 | } catch (error) { 84 | console.log(error); 85 | resolve({ 86 | success: false, 87 | message: `${importPath} 依赖没有安装,请检查大小写或路径错误`, 88 | }); 89 | } 90 | } 91 | } 92 | }, 93 | }); 94 | resolve({ 95 | success: true, 96 | }); 97 | return; 98 | }); 99 | }; 100 | 101 | const forEachFile = (code, filePath) => { 102 | const ast = parser.parse(code, { 103 | sourceType: 'module', 104 | plugins: ['jsx', 'typescript', 'dynamicImport', 'classProperties', 'decorators-legacy'], 105 | }); 106 | return checkDepsByAst(ast, filePath); 107 | }; 108 | 109 | const globList = (patternList, options) => { 110 | let fileList = []; 111 | patternList.forEach((pattern) => { 112 | fileList = [...fileList, ...glob.sync(pattern, options)]; 113 | }); 114 | 115 | return fileList; 116 | }; 117 | const checkDeps = ({ cwd }) => { 118 | console.log(cwd); 119 | // 寻找项目下的所有 ts 120 | spinner.start('🕵️‍ find all code files'); 121 | const tsFiles = globList(['packages/**/src/**/*.tsx', 'packages/**/src/**/*.tsx'], { 122 | cwd, 123 | ignore: [ 124 | '**/*.d.ts', 125 | '**/demos/**', 126 | '**/dist/**', 127 | '**/public/**', 128 | '**/locales/**', 129 | '**/node_modules/**', 130 | ], 131 | }); 132 | spinner.succeed(); 133 | 134 | const getFileContent = (path) => fs.readFileSync(slash(path), 'utf-8'); 135 | 136 | spinner.start('🕵️ check deps'); 137 | 138 | tsFiles.forEach(async (path) => { 139 | const source = getFileContent(slash(join(cwd, path))); 140 | if (source.includes('import')) { 141 | const result = await forEachFile(source, path).catch(() => {}); 142 | if (result.success === false) { 143 | console.log(`😂 ${path} 发现了错误:\n ${result.message}`); 144 | process.exit(2); 145 | } 146 | } 147 | }); 148 | spinner.succeed(); 149 | }; 150 | 151 | /** 检查所有的根目录文件 */ 152 | checkDeps({ 153 | cwd: slash(join(__dirname, '..')), 154 | }); 155 | -------------------------------------------------------------------------------- /scripts/createRelease.js: -------------------------------------------------------------------------------- 1 | const GitHub = require('github'); 2 | const exec = require('child_process').execSync; 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const github = new GitHub({ 7 | debug: process.env.NODE_ENV === 'development', 8 | }); 9 | 10 | github.authenticate({ 11 | type: 'token', 12 | token: process.env.GITHUB_TOKEN || process.env.GITHUB_AUTH, 13 | }); 14 | 15 | const getChangelog = (content, version) => { 16 | const lines = content.split('\n'); 17 | const changeLog = []; 18 | const startPattern = new RegExp(`^## ${version}`); 19 | const stopPattern = /^## /; // 前一个版本 20 | const skipPattern = /^`/; // 日期 21 | let begin = false; 22 | for (let i = 0; i < lines.length; i += 1) { 23 | const line = lines[i]; 24 | if (begin && stopPattern.test(line)) { 25 | break; 26 | } 27 | if (begin && line && !skipPattern.test(line)) { 28 | changeLog.push(line); 29 | } 30 | if (!begin) { 31 | begin = startPattern.test(line); 32 | } 33 | } 34 | return changeLog.join('\n'); 35 | }; 36 | 37 | const getMds = (allVersion = false) => { 38 | const docDir = path.join(__dirname, '..', 'docs'); 39 | const mdFils = fs.readdirSync(docDir).filter((name) => name.includes('changelog.md')); 40 | mdFils.map((mdFile) => { 41 | const pkg = mdFile.replace('pro-', '').replace('.changelog.md', ''); 42 | const content = fs.readFileSync(path.join(docDir, mdFile)).toString(); 43 | let versions = [ 44 | require(path.join(path.join(__dirname, '..', 'packages', pkg, 'package.json'))).version, 45 | ]; 46 | if (allVersion) { 47 | versions = exec('git tag') 48 | .toString() 49 | .split('\n') 50 | .filter((tag) => tag.includes(pkg)) 51 | .map((tag) => tag.split('@').pop()); 52 | } 53 | console.log(versions); 54 | versions.map((version) => { 55 | const versionPkg = `@ant-design/pro-${pkg}@${version}`; 56 | const changeLog = getChangelog(content, versionPkg); 57 | if (!changeLog) { 58 | return; 59 | } 60 | github.repos 61 | .createRelease({ 62 | owner: 'ant-design', 63 | repo: 'pro-components', 64 | tag_name: versionPkg, 65 | name: versionPkg, 66 | body: changeLog, 67 | }) 68 | .catch((e) => { 69 | console.log(e); 70 | }); 71 | }); 72 | }); 73 | }; 74 | 75 | getMds(); 76 | -------------------------------------------------------------------------------- /scripts/gen_less_entry.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { join } = require('path'); 3 | const fg = require('fast-glob'); 4 | // 用于转换 Windows 反斜杠路径转换为正斜杠路径 \ => / 5 | const slash = require('slash'); 6 | 7 | const pkgList = fs 8 | .readdirSync(join(__dirname, '../', 'packages')) 9 | .filter((pkg) => pkg.charAt(0) !== '.'); 10 | 11 | pkgList.map(async (path) => { 12 | const baseUrl = slash(`${join(__dirname, '../', 'packages')}/${path}/src`); 13 | const lessFiles = await fg(`${baseUrl}/**/*.less`, { 14 | ignore: ['**/demos/**'], 15 | deep: 5, 16 | }); 17 | 18 | const importFiles = lessFiles.map((lessPath) => { 19 | return `@import "../es${lessPath.replace(baseUrl, '')}";`; 20 | }); 21 | 22 | const distPath = slash(`${join(__dirname, '../', 'packages', path, 'dist', `${path}.less`)}`); 23 | // console.log(11, distPath, importFiles) 24 | fs.writeFileSync(distPath, importFiles.join('\n')); 25 | }); 26 | -------------------------------------------------------------------------------- /scripts/generateSizeLimit.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | const writePkg = require('write-pkg'); 4 | 5 | const cwd = process.cwd(); 6 | const ignoreDirPath = ['.DS_Store']; 7 | 8 | const filePath = path.resolve(cwd, 'package.json'); 9 | const packagesDir = path.resolve(cwd, 'packages'); 10 | const json = JSON.parse(fs.readFileSync(filePath, 'utf8')); 11 | 12 | delete json['size-limit']; 13 | 14 | let componentsNames = fs.readdirSync(packagesDir); 15 | 16 | componentsNames = componentsNames.filter((dir) => ignoreDirPath.indexOf(dir) === -1); 17 | 18 | (async () => { 19 | const sizeLimitConfig = []; 20 | componentsNames.forEach((component) => { 21 | sizeLimitConfig.push({ 22 | path: `packages/${component}/lib/**/*.js`, 23 | limit: '2 s', 24 | webpack: false, 25 | running: false, 26 | }); 27 | sizeLimitConfig.push({ 28 | path: `packages/${component}/es/**/*.js`, 29 | limit: '2 s', 30 | webpack: false, 31 | running: false, 32 | }); 33 | }); 34 | 35 | await writePkg(cwd, { ...json, 'size-limit': sizeLimitConfig }); 36 | })(); 37 | -------------------------------------------------------------------------------- /scripts/issue.js: -------------------------------------------------------------------------------- 1 | const Octokit = require('@octokit/core'); 2 | 3 | const octokit = new Octokit.Octokit({ 4 | auth: process.env.GITHUB_TOKEN || process.env.GITHUB_AUTH, 5 | }); 6 | 7 | const queryIssue = ({ title, id }) => { 8 | return octokit 9 | .request('GET /search/issues', { 10 | q: title, 11 | per_page: 5, 12 | }) 13 | .then(({ data }) => { 14 | const list = data.items 15 | .map((item) => { 16 | return { 17 | title: item.title, 18 | url: item.html_url, 19 | id: item.id, 20 | }; 21 | }) 22 | .filter((item) => { 23 | return item.id !== id; 24 | }); 25 | 26 | if (list.length > 0) { 27 | return ` 28 | > Issue Robot generation 29 | 30 | ### 以下的issue可能会帮助到你 : 31 | 32 | ${list 33 | .map((item) => { 34 | return `* [${item.title}](${item.url})`; 35 | }) 36 | .join('\n')}`; 37 | } 38 | return null; 39 | }) 40 | .then(async (markdown) => { 41 | return markdown; 42 | }); 43 | }; 44 | 45 | const findIssue = async (issueId) => { 46 | const { data } = await octokit.request('GET /repos/{owner}/{repo}/issues/{issue_number}', { 47 | owner: 'ant-design', 48 | repo: 'pro-components', 49 | issue_number: issueId, 50 | }); 51 | return data; 52 | }; 53 | const closeIssue = async (issueId) => { 54 | await octokit.request('PATCH /repos/{owner}/{repo}/issues/{issue_number}', { 55 | owner: 'ant-design', 56 | repo: 'pro-components', 57 | issue_number: issueId, 58 | state: 'closed', 59 | }); 60 | }; 61 | const replyCommit = async (issueId, markdown) => { 62 | await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', { 63 | owner: 'ant-design', 64 | repo: 'pro-components', 65 | issue_number: issueId, 66 | body: markdown, 67 | }); 68 | }; 69 | 70 | const reply = async () => { 71 | const issueId = process.env.ISSUE_NUMBER; 72 | const issue = await findIssue(issueId); 73 | if (!issue.title || issue.title.length < 12) { 74 | replyCommit(issueId, '**请写一个可读的标题!**'); 75 | closeIssue(issueId); 76 | return; 77 | } 78 | // const markdown = await queryIssue({ 79 | // title: issue.title, 80 | // id: issue.id, 81 | // }); 82 | // replyCommit(issueId, markdown); 83 | }; 84 | 85 | reply(); 86 | -------------------------------------------------------------------------------- /scripts/preDeploy.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readdirSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | (async () => { 5 | const pkgs = readdirSync(join(__dirname, '../packages')).filter((pkg) => pkg.charAt(0) !== '.'); 6 | 7 | pkgs.forEach((shortName) => { 8 | const distPath = join(__dirname, '..', 'packages', shortName, 'dist'); 9 | const distExists = existsSync(distPath); 10 | if (!distExists) { 11 | // 如果没有先生成 dist 目录,dumi build 之后,在 codesandbox 里面会找不到 css 样式。 12 | console.error('Please execute "yarn build" first!'); 13 | process.exit(1); 14 | } 15 | }); 16 | })(); 17 | -------------------------------------------------------------------------------- /scripts/release.js: -------------------------------------------------------------------------------- 1 | const { utils } = require('umi'); 2 | const { join } = require('path'); 3 | const exec = require('./utils/exec'); 4 | const inquirer = require('inquirer'); 5 | const getPackages = require('./utils/getPackages'); 6 | const isNextVersion = require('./utils/isNextVersion'); 7 | 8 | const { yParser, execa, chalk } = utils; 9 | const cwd = process.cwd(); 10 | const args = yParser(process.argv); 11 | const lernaCli = require.resolve('lerna/cli'); 12 | 13 | function printErrorAndExit(message) { 14 | console.error(chalk.red(message)); 15 | process.exit(1); 16 | } 17 | 18 | function logStep(name) { 19 | console.log(`${chalk.gray('>> Release:')} ${chalk.magenta.bold(name)}`); 20 | } 21 | 22 | function packageExists({ name, version }) { 23 | const { stdout } = execa.sync('npm', ['info', `${name}@${version}`]); 24 | return stdout.length > 0; 25 | } 26 | 27 | async function release() { 28 | // Check git status 29 | if (!args.skipGitStatusCheck) { 30 | const gitStatus = execa.sync('git', ['status', '--porcelain']).stdout; 31 | if (gitStatus.length) { 32 | printErrorAndExit(`Your git status is not clean. Aborting.`); 33 | } 34 | } else { 35 | logStep('git status check is skipped, since --skip-git-status-check is supplied'); 36 | } 37 | 38 | // Check npm registry 39 | logStep('check npm registry'); 40 | const userRegistry = execa.sync('npm', ['config', 'get', 'registry']).stdout; 41 | if (userRegistry.includes('https://registry.yarnpkg.com/')) { 42 | printErrorAndExit(`Release failed, please use ${chalk.blue('npm run release')}.`); 43 | } 44 | if (!userRegistry.includes('https://registry.npmjs.org/')) { 45 | const registry = chalk.blue('https://registry.npmjs.org/'); 46 | printErrorAndExit(`Release failed, npm registry must be ${registry}.`); 47 | } 48 | 49 | let updated = null; 50 | 51 | if (!args.publishOnly) { 52 | // Get updated packages 53 | logStep('check updated packages'); 54 | const updatedStdout = execa.sync(lernaCli, ['changed']).stdout; 55 | updated = updatedStdout 56 | .split('\n') 57 | .map((pkg) => { 58 | return pkg.split('/')[1]; 59 | }) 60 | .filter(Boolean); 61 | if (!updated.length) { 62 | printErrorAndExit('Release failed, no updated package is updated.'); 63 | } 64 | 65 | // Clean 66 | logStep('clean'); 67 | 68 | // Build 69 | if (!args.skipBuild) { 70 | logStep('build'); 71 | await exec('npm', ['run', 'build']); 72 | } else { 73 | logStep('build is skipped, since args.skipBuild is supplied'); 74 | } 75 | 76 | // Bump version 77 | // Commit 78 | // Git Tag 79 | // Push 80 | logStep('bump version with lerna version'); 81 | 82 | const conventionalGraduate = args.conventionalGraduate 83 | ? ['--conventional-graduate'].concat( 84 | Array.isArray(args.conventionalGraduate) ? args.conventionalGraduate.join(',') : [], 85 | ) 86 | : []; 87 | const conventionalPrerelease = args.conventionalPrerelease 88 | ? ['--conventional-prerelease'].concat( 89 | Array.isArray(args.conventionalPrerelease) ? args.conventionalPrerelease.join(',') : [], 90 | ) 91 | : []; 92 | 93 | await exec( 94 | 'node', 95 | [ 96 | [lernaCli], 97 | 'version', 98 | '--exact', 99 | // '--no-commit-hooks', 100 | // '--no-git-tag-version', 101 | // '--no-push', 102 | '--message', 103 | '🎨 chore(release): Publish', 104 | '--conventional-commits', 105 | ] 106 | .concat(conventionalGraduate) 107 | .concat(conventionalPrerelease), 108 | { 109 | shell: false, 110 | }, 111 | ); 112 | } 113 | 114 | // Publish 115 | // Umi must be the latest. 116 | const pkgs = args.publishOnly ? getPackages() : updated; 117 | logStep(`publish packages: ${chalk.blue(pkgs.join(', '))}`); 118 | 119 | // 获取 opt 的输入 120 | const { otp } = await inquirer.prompt([ 121 | { 122 | type: 'input', 123 | name: 'otp', 124 | message: '请输入 otp 的值,留空表示不使用 otp', 125 | }, 126 | ]); 127 | 128 | process.env.NPM_CONFIG_OTP = otp; 129 | 130 | pkgs.forEach((pkg, index) => { 131 | const pkgPath = join(cwd, 'packages', pkg.replace('pro-', '')); 132 | const { name, version } = require(join(pkgPath, 'package.json')); 133 | const isNext = isNextVersion(version); 134 | let isPackageExist = null; 135 | if (args.publishOnly) { 136 | isPackageExist = packageExists({ name, version }); 137 | if (isPackageExist) { 138 | console.log(`package ${name}@${version} is already exists on npm, skip.`); 139 | } 140 | } 141 | if (!args.publishOnly || !isPackageExist) { 142 | console.log( 143 | `[${index + 1}/${pkgs.length}] Publish package ${name} ${isNext ? 'with next tag' : ''}`, 144 | ); 145 | const cliArgs = isNext ? ['publish', '--tag', 'next'] : ['publish']; 146 | const { stdout } = execa.sync('npm', cliArgs, { 147 | cwd: pkgPath, 148 | }); 149 | console.log(stdout); 150 | } 151 | }); 152 | 153 | await exec('npm', ['run', 'prettier']); 154 | 155 | logStep('done'); 156 | } 157 | 158 | release().catch((err) => { 159 | console.error(err); 160 | process.exit(1); 161 | }); 162 | -------------------------------------------------------------------------------- /scripts/replaceLib.js: -------------------------------------------------------------------------------- 1 | const { join, dirname } = require('path'); 2 | const fs = require('fs'); 3 | 4 | const cwd = process.cwd(); 5 | 6 | function replacePath(path) { 7 | if (path.node.source && /\/lib\//.test(path.node.source.value)) { 8 | const esModule = path.node.source.value.replace('/lib/', '/es/'); 9 | const esPath = dirname(join(cwd, `node_modules/${esModule}`)); 10 | if (fs.existsSync(esPath)) { 11 | console.log(`[es build] replace ${path.node.source.value} with ${esModule}`); 12 | path.node.source.value = esModule; 13 | } 14 | } 15 | } 16 | 17 | function replaceLib() { 18 | return { 19 | visitor: { 20 | ImportDeclaration: replacePath, 21 | ExportNamedDeclaration: replacePath, 22 | }, 23 | }; 24 | } 25 | module.exports = replaceLib; 26 | -------------------------------------------------------------------------------- /scripts/syncTNPM.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const { execa } = require('@umijs/utils'); 3 | const { join } = require('path'); 4 | const getPackages = require('./utils/getPackages'); 5 | 6 | process.setMaxListeners(Infinity); 7 | 8 | module.exports = (publishPkgList) => { 9 | const pkgList = (publishPkgList || getPackages()).map((name) => { 10 | // eslint-disable-next-line import/no-dynamic-require 11 | return require(join(__dirname, '../packages', name, 'package.json')).name; 12 | }); 13 | const commands = pkgList.map((pkg) => { 14 | const subprocess = execa('tnpm', ['sync', pkg]); 15 | subprocess.stdout.pipe(process.stdout); 16 | return subprocess; 17 | }); 18 | Promise.all(commands); 19 | }; 20 | -------------------------------------------------------------------------------- /scripts/utils/exec.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | module.exports = function exec(command, args, opts) { 4 | return new Promise((resolve, reject) => { 5 | const child = spawn(command, args, { 6 | shell: true, 7 | stdio: 'inherit', 8 | env: process.env, 9 | ...opts, 10 | }); 11 | child.once('error', (err) => { 12 | console.log(err); 13 | reject(err); 14 | }); 15 | child.once('close', (code) => { 16 | if (code === 1) { 17 | process.exit(1); 18 | } else { 19 | resolve(); 20 | } 21 | }); 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /scripts/utils/getPackages.js: -------------------------------------------------------------------------------- 1 | const { readdirSync } = require('fs'); 2 | const { join } = require('path'); 3 | 4 | module.exports = function getPackages() { 5 | return readdirSync(join(__dirname, '../../packages')).filter((pkg) => pkg.charAt(0) !== '.'); 6 | }; 7 | -------------------------------------------------------------------------------- /scripts/utils/isNextVersion.js: -------------------------------------------------------------------------------- 1 | module.exports = function (version) { 2 | return version.includes('-rc.') || version.includes('-beta.') || version.includes('-alpha.'); 3 | }; 4 | -------------------------------------------------------------------------------- /scripts/verifyCommit.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | // Invoked on the commit-msg git hook by yorkie. 3 | 4 | const { chalk } = require('@umijs/utils'); 5 | 6 | const msgPath = process.env.GIT_PARAMS; 7 | const msg = require('fs').readFileSync(msgPath, 'utf-8').trim(); 8 | 9 | const commitRE = 10 | /^(((\ud83c[\udf00-\udfff])|(\ud83d[\udc00-\ude4f\ude80-\udeff])|[\u2600-\u2B55]) )?(revert: )?(feat|fix|docs|UI|refactor|⚡perf|workflow|build|CI|typos|chore|tests|types|wip|release|dep)(\(.+\))?: .{1,50}/; 11 | 12 | if (!commitRE.test(msg)) { 13 | console.log(); 14 | console.error( 15 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red( 16 | `invalid commit message format.`, 17 | )}\n\n${chalk.red( 18 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n`, 19 | )} 20 | ${chalk.green(`💥 feat(compiler): add 'comments' option`)}\n 21 | ${chalk.green(`🐛 fix(compiler): fix some bug`)}\n 22 | ${chalk.green(`📝 docs(compiler): add some docs`)}\n 23 | ${chalk.green(`💄 UI(compiler): better styles`)}\n 24 | ${chalk.green(`🎨 chore(compiler): do something`)}\n 25 | ${chalk.red(`See .github/commit-convention.md for more details.\n`)}`, 26 | ); 27 | process.exit(1); 28 | } 29 | -------------------------------------------------------------------------------- /tests/card/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('card'); 4 | -------------------------------------------------------------------------------- /tests/card/index.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import ProCard from '@ant-design/pro-card'; 4 | import { waitForComponentToPaint } from '../util'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { Grid } from 'antd'; 7 | 8 | jest.mock('antd/lib/grid/hooks/useBreakpoint'); 9 | 10 | describe('Card', () => { 11 | it('🥩 collapsible onCollapse', async () => { 12 | const fn = jest.fn(); 13 | const wrapper = mount( 14 | 15 | 内容 16 | , 17 | ); 18 | await waitForComponentToPaint(wrapper); 19 | act(() => { 20 | wrapper.find('AntdIcon.ant-pro-card-collapsible-icon').simulate('click'); 21 | }); 22 | expect(fn).toBeCalled(); 23 | }); 24 | 25 | it('🥩 resize breakpoint', async () => { 26 | // @ts-ignore 27 | Grid.useBreakpoint.mockReturnValue({ xs: true }); 28 | 29 | const wrapper = mount( 30 | 35 | Col 36 | , 37 | ); 38 | 39 | await waitForComponentToPaint(wrapper); 40 | }); 41 | 42 | it('🥩 collapsible defaultCollapsed', async () => { 43 | const wrapper = mount( 44 | 45 | 内容 46 | , 47 | ); 48 | await waitForComponentToPaint(wrapper); 49 | expect(wrapper.find('.ant-pro-card-collapse').exists()).toBeTruthy(); 50 | }); 51 | 52 | it('🥩 collapsible collapsed', async () => { 53 | const wrapper = mount( 54 | 55 | 内容 56 | , 57 | ); 58 | await waitForComponentToPaint(wrapper); 59 | expect(wrapper.find('.ant-pro-card-collapse').exists()).toBeTruthy(); 60 | 61 | act(() => { 62 | wrapper.setProps({ 63 | collapsed: false, 64 | }); 65 | }); 66 | 67 | await waitForComponentToPaint(wrapper); 68 | expect(wrapper.find('.ant-pro-card-collapse').exists()).toBeFalsy(); 69 | }); 70 | 71 | it('🥩 tabs onChange', async () => { 72 | const fn = jest.fn(); 73 | const wrapper = mount( 74 | 79 | 80 | 内容一 81 | 82 | 83 | 内容二 84 | 85 | , 86 | ); 87 | act(() => { 88 | wrapper.find('.ant-pro-card-tabs .ant-tabs-tab').at(1).simulate('click'); 89 | }); 90 | expect(fn).toHaveBeenCalledWith('tab2'); 91 | }); 92 | }); 93 | -------------------------------------------------------------------------------- /tests/demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import glob from 'glob'; 3 | import type { render } from 'enzyme'; 4 | import { mount } from 'enzyme'; 5 | import MockDate from 'mockdate'; 6 | import moment from 'moment'; 7 | import { waitForComponentToPaint } from './util'; 8 | 9 | type CheerIO = ReturnType; 10 | type CheerIOElement = CheerIO[0]; 11 | // We should avoid use it in 4.0. Reopen if can not handle this. 12 | const USE_REPLACEMENT = false; 13 | const testDist = process.env.LIB_DIR === 'dist'; 14 | 15 | /** 16 | * Rc component will generate id for aria usage. It's created as `test-uuid` when env === 'test'. Or 17 | * `f7fa7a3c-a675-47bc-912e-0c45fb6a74d9`(randomly) when not test env. So we need hack of this to 18 | * modify the `aria-controls`. 19 | */ 20 | function ariaConvert(wrapper: CheerIO) { 21 | if (!testDist || !USE_REPLACEMENT) return wrapper; 22 | 23 | const matches = new Map(); 24 | 25 | function process(entity: any) { 26 | const { attribs, children } = entity; 27 | if (matches.has(entity)) return; 28 | matches.set(entity, true); 29 | 30 | // Change aria 31 | if (attribs && attribs['aria-controls']) { 32 | attribs['aria-controls'] = ''; // Remove all the aria to keep render sync in jest & jest node 33 | } 34 | 35 | // Loop children 36 | if (!children) return; 37 | (Array.isArray(children) ? children : [children]).forEach(process); 38 | } 39 | 40 | wrapper.each((_: any, entity: CheerIOElement) => process(entity)); 41 | 42 | return wrapper; 43 | } 44 | 45 | type Options = { 46 | skip?: boolean; 47 | }; 48 | 49 | function demoTest(component: string, options: Options = {}) { 50 | const LINE_STR_COUNT = 20; 51 | const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); 52 | const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); 53 | 54 | // Mock offsetHeight 55 | // @ts-expect-error 56 | const originOffsetHeight = Object.getOwnPropertyDescriptor( 57 | HTMLElement.prototype, 58 | 'offsetHeight', 59 | ).get; 60 | Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { 61 | get() { 62 | let html = this.innerHTML; 63 | html = html.replace(/<[^>]*>/g, ''); 64 | const lines = Math.ceil(html.length / LINE_STR_COUNT); 65 | return lines * 16; 66 | }, 67 | }); 68 | 69 | // Mock getComputedStyle 70 | const originGetComputedStyle = window.getComputedStyle; 71 | window.getComputedStyle = (ele) => { 72 | const style = originGetComputedStyle(ele); 73 | style.lineHeight = '16px'; 74 | return style; 75 | }; 76 | 77 | afterEach(() => { 78 | logSpy.mockReset(); 79 | errorSpy.mockReset(); 80 | }); 81 | 82 | afterAll(() => { 83 | errorSpy.mockRestore(); 84 | logSpy.mockReset(); 85 | Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { 86 | get: originOffsetHeight, 87 | }); 88 | window.getComputedStyle = originGetComputedStyle; 89 | }); 90 | // 支持 demos 下的所有非_开头的tsx文件 91 | const files = glob.sync(`./packages/${component}/**/demos/**/[!_]*.tsx`); 92 | files.push(...glob.sync(`./${component}/**/**/demos/[!_]*.tsx`)); 93 | 94 | describe(`${component} demos`, () => { 95 | files.forEach((file) => { 96 | let testMethod = options.skip === true ? test.skip : test; 97 | if (Array.isArray(options.skip) && options.skip.some((c) => file.includes(c))) { 98 | testMethod = test.skip; 99 | } 100 | testMethod(`📸 renders ${file} correctly`, async () => { 101 | MockDate.set(moment('2016-11-22').valueOf()); 102 | const Demo = require(`.${file}`).default; // eslint-disable-line global-require, import/no-dynamic-require 103 | const wrapper = mount(); 104 | await waitForComponentToPaint(wrapper, ['table', 'list'].includes(component) ? 3000 : 1000); 105 | // Convert aria related content 106 | const dom = wrapper.render(); 107 | ariaConvert(dom); 108 | expect(dom).toMatchSnapshot(); 109 | MockDate.reset(); 110 | }); 111 | }); 112 | }); 113 | } 114 | 115 | export default demoTest; 116 | -------------------------------------------------------------------------------- /tests/descriptions/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('descriptions'); 4 | -------------------------------------------------------------------------------- /tests/doc.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from './demo'; 2 | 3 | demoTest('docs'); 4 | -------------------------------------------------------------------------------- /tests/field/__snapshots__/status.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Field Status 🥩 Default render 1`] = ` 4 | 7 | 10 | 13 | 未解决 14 | 15 | 16 | `; 17 | 18 | exports[`Field Status 🥩 Error render 1`] = ` 19 | 22 | 25 | 28 | 未解决 29 | 30 | 31 | `; 32 | 33 | exports[`Field Status 🥩 Processing render 1`] = ` 34 | 37 | 40 | 43 | 未解决 44 | 45 | 46 | `; 47 | 48 | exports[`Field Status 🥩 Success render 1`] = ` 49 | 52 | 55 | 58 | 未解决 59 | 60 | 61 | `; 62 | 63 | exports[`Field Status 🥩 Warning render 1`] = ` 64 | 67 | 70 | 73 | 未解决 74 | 75 | 76 | `; 77 | 78 | exports[`Field Status 🥩 default render 1`] = ` 79 | 82 | 85 | 88 | 未解决 89 | 90 | 91 | `; 92 | 93 | exports[`Field Status 🥩 error render 1`] = ` 94 | 97 | 100 | 103 | 未解决 104 | 105 | 106 | `; 107 | 108 | exports[`Field Status 🥩 processing render 1`] = ` 109 | 112 | 115 | 118 | 未解决 119 | 120 | 121 | `; 122 | 123 | exports[`Field Status 🥩 red color render 1`] = ` 124 | 127 | 130 | 133 | 未解决 134 | 135 | 136 | `; 137 | 138 | exports[`Field Status 🥩 success render 1`] = ` 139 | 142 | 145 | 148 | 未解决 149 | 150 | 151 | `; 152 | 153 | exports[`Field Status 🥩 warning render 1`] = ` 154 | 157 | 160 | 163 | 未解决 164 | 165 | 166 | `; 167 | -------------------------------------------------------------------------------- /tests/field/datePick.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { act } from 'react-dom/test-utils'; 4 | import Field from '@ant-design/pro-field'; 5 | import moment from 'moment'; 6 | import { waitForComponentToPaint } from '../util'; 7 | 8 | describe('Field', () => { 9 | const datePickList = ['date', 'dateWeek', 'dateMonth', 'dateQuarter', 'dateYear', 'dateTime']; 10 | datePickList.forEach((valueType) => { 11 | it(`📅 ${valueType} base use`, async () => { 12 | const fn = jest.fn(); 13 | const html = mount( 14 | , 24 | ); 25 | act(() => { 26 | html.find('.ant-pro-core-field-label').simulate('mousedown'); 27 | }); 28 | 29 | await waitForComponentToPaint(html, 100); 30 | 31 | act(() => { 32 | html.find('.anticon-close').simulate('click'); 33 | }); 34 | await waitForComponentToPaint(html, 100); 35 | expect(fn).toBeCalled(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/field/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('field'); 4 | -------------------------------------------------------------------------------- /tests/field/status.test.tsx: -------------------------------------------------------------------------------- 1 | import { render } from 'enzyme'; 2 | import React from 'react'; 3 | import Field from '@ant-design/pro-field'; 4 | 5 | describe('Field Status', () => { 6 | const statusList = [ 7 | 'Success', 8 | 'Error', 9 | 'Processing', 10 | 'Default', 11 | 'Warning', 12 | 'success', 13 | 'error', 14 | 'processing', 15 | 'default', 16 | 'warning', 17 | ]; 18 | statusList.forEach((status) => { 19 | it(`🥩 ${status} render`, async () => { 20 | const html = render( 21 | , 31 | ); 32 | expect(html).toMatchSnapshot(); 33 | }); 34 | }); 35 | 36 | it(`🥩 red color render`, async () => { 37 | const html = render( 38 | , 48 | ); 49 | expect(html).toMatchSnapshot(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/form/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('form'); 4 | -------------------------------------------------------------------------------- /tests/form/formitem.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProForm, { ProFormText } from '@ant-design/pro-form'; 3 | import { act } from 'react-dom/test-utils'; 4 | 5 | import { mount } from 'enzyme'; 6 | import { waitForComponentToPaint } from '../util'; 7 | import { Input } from 'antd'; 8 | 9 | describe('ProForm.Item', () => { 10 | it('📦 ProForm support fieldProps.onBlur', async () => { 11 | const onBlur = jest.fn(); 12 | const wrapper = mount<{ navTheme: string }>( 13 | 18 | onBlur(e.target.value), 22 | }} 23 | name="navTheme" 24 | /> 25 | , 26 | ); 27 | await waitForComponentToPaint(wrapper); 28 | act(() => { 29 | wrapper.find('input#navTheme').simulate('focus'); 30 | }); 31 | await waitForComponentToPaint(wrapper); 32 | act(() => { 33 | wrapper.find('input#navTheme').simulate('blur'); 34 | }); 35 | 36 | expect(onBlur).toBeCalledWith('dark'); 37 | expect(onBlur).toBeCalledTimes(1); 38 | }); 39 | 40 | it('📦 ProForm.Item supports onChange', async () => { 41 | const onChange = jest.fn(); 42 | const onValuesChange = jest.fn(); 43 | const wrapper = mount<{ navTheme: string }>( 44 | onValuesChange(name)} 49 | > 50 | 51 | onChange(e.target.value)} id="name" /> 52 | 53 | , 54 | ); 55 | await waitForComponentToPaint(wrapper); 56 | 57 | act(() => { 58 | wrapper.find('input#name').simulate('change', { 59 | target: { 60 | value: '1212', 61 | }, 62 | }); 63 | }); 64 | 65 | expect(onChange).toBeCalledWith('1212'); 66 | expect(onChange).toBeCalledTimes(1); 67 | expect(onValuesChange).toBeCalledWith('1212'); 68 | expect(onValuesChange).toBeCalledTimes(1); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/form/loginForm.test.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm, ProFormText } from '@ant-design/pro-form'; 2 | import { mount } from 'enzyme'; 3 | import { waitForComponentToPaint } from '../util'; 4 | import { AlipayCircleOutlined, TaobaoCircleOutlined, WeiboCircleOutlined } from '@ant-design/icons'; 5 | import { Alert, Space } from 'antd'; 6 | 7 | describe('LoginForm', () => { 8 | it('📦 LoginForm should show login message correctly', async () => { 9 | const loginMessage = ; 10 | 11 | const wrapper = mount( 12 | 13 | 14 | , 15 | ); 16 | await waitForComponentToPaint(wrapper); 17 | 18 | expect(wrapper.find('.ant-alert.ant-alert-error').length).toEqual(1); 19 | expect(wrapper.find('.ant-alert.ant-alert-error .ant-alert-message').text()).toEqual( 20 | '登录失败', 21 | ); 22 | }); 23 | 24 | it('📦 LoginForm should render actions correctly', async () => { 25 | const wrapper = mount( 26 | 29 | 其他登录方式 30 | 31 | 32 | 33 | 34 | } 35 | > 36 | 37 | , 38 | ); 39 | await waitForComponentToPaint(wrapper); 40 | 41 | expect(wrapper.find('.ant-pro-form-login-other .anticon').length).toEqual(3); 42 | }); 43 | 44 | it('📦 LoginForm support string logo', async () => { 45 | const wrapper = mount( 46 | 47 | 48 | , 49 | ); 50 | await waitForComponentToPaint(wrapper); 51 | 52 | expect(wrapper.find('.ant-pro-form-login-logo img').exists()).toBeTruthy(); 53 | expect(wrapper.find('.ant-pro-form-login-logo img').props().src).toBe( 54 | 'https://avatars.githubusercontent.com/u/8186664?v=4', 55 | ); 56 | }); 57 | 58 | it('📦 LoginForm support react node logo', async () => { 59 | const wrapper = mount( 60 | }> 61 | 62 | , 63 | ); 64 | await waitForComponentToPaint(wrapper); 65 | 66 | expect(wrapper.find('.ant-pro-form-login-logo #test').exists()).toBeTruthy(); 67 | expect(wrapper.find('.ant-pro-form-login-logo #test').props().src).toBe( 68 | 'https://avatars.githubusercontent.com/u/8186664?v=4', 69 | ); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/form/proFormMoney.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ProForm, { ProFormMoney } from '@ant-design/pro-form'; 3 | import { act } from 'react-dom/test-utils'; 4 | 5 | import { mount } from 'enzyme'; 6 | import { waitForComponentToPaint } from '../util'; 7 | import { ConfigProvider } from 'antd'; 8 | import enGBIntl from 'antd/lib/locale/en_GB'; 9 | 10 | describe('$ ProFormMoney', () => { 11 | it('$ ProFormMoney value expect number', async () => { 12 | const fn = jest.fn(); 13 | const wrapper = mount<{ amount: string }>( 14 | { 16 | fn(values.amount); 17 | }} 18 | > 19 | 20 | , 21 | ); 22 | await waitForComponentToPaint(wrapper); 23 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe('¥'); 24 | act(() => { 25 | wrapper.find('button.ant-btn-primary').simulate('click'); 26 | }); 27 | await waitForComponentToPaint(wrapper); 28 | expect(fn).toHaveBeenCalledWith(44.33); 29 | expect(wrapper.render()).toMatchSnapshot(); 30 | }); 31 | it('$ moneySymbol with global locale', async () => { 32 | const fn = jest.fn(); 33 | const wrapper = mount<{ amount: string }>( 34 | 35 | { 37 | fn(values.amount); 38 | }} 39 | > 40 | 41 | 42 | , 43 | ); 44 | await waitForComponentToPaint(wrapper); 45 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe('£'); 46 | act(() => { 47 | wrapper.find('button.ant-btn-primary').simulate('click'); 48 | }); 49 | await waitForComponentToPaint(wrapper); 50 | expect(fn).toHaveBeenCalledWith(44.33); 51 | expect(wrapper.render()).toMatchSnapshot(); 52 | }); 53 | 54 | it('$ moneySymbol with custom locale', async () => { 55 | const fn = jest.fn(); 56 | const wrapper = mount<{ amount: string }>( 57 | { 59 | fn(values.amount); 60 | }} 61 | > 62 | 63 | , 64 | ); 65 | await waitForComponentToPaint(wrapper); 66 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe('$'); 67 | act(() => { 68 | wrapper.find('button.ant-btn-primary').simulate('click'); 69 | }); 70 | await waitForComponentToPaint(wrapper); 71 | expect(fn).toHaveBeenCalledWith(44.33); 72 | expect(wrapper.render()).toMatchSnapshot(); 73 | }); 74 | it('$ moneySymbol with custom symbol', async () => { 75 | const fn = jest.fn(); 76 | const wrapper = mount<{ amount: string }>( 77 | { 79 | fn(values.amount); 80 | }} 81 | > 82 | 83 | , 84 | ); 85 | await waitForComponentToPaint(wrapper); 86 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 2)).toBe('💰'); 87 | act(() => { 88 | wrapper.find('button.ant-btn-primary').simulate('click'); 89 | }); 90 | await waitForComponentToPaint(wrapper); 91 | expect(fn).toHaveBeenCalledWith(44.33); 92 | expect(wrapper.render()).toMatchSnapshot(); 93 | }); 94 | it('$ can not input negative', async () => { 95 | const fn = jest.fn(); 96 | const wrapper = mount<{ amount: string }>( 97 | { 99 | fn(values.amount); 100 | }} 101 | > 102 | 103 | , 104 | ); 105 | await waitForComponentToPaint(wrapper); 106 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe(''); 107 | act(() => { 108 | wrapper.find('input#amount').simulate('change', { 109 | target: { 110 | value: '-55.33', 111 | }, 112 | }); 113 | }); 114 | await waitForComponentToPaint(wrapper, 300); 115 | act(() => { 116 | wrapper.find('button.ant-btn-primary').simulate('click'); 117 | }); 118 | await waitForComponentToPaint(wrapper, 300); 119 | expect(fn).toHaveBeenCalledWith(undefined); 120 | expect(wrapper.render()).toMatchSnapshot(); 121 | }); 122 | it('$ can input negative', async () => { 123 | const fn = jest.fn(); 124 | const wrapper = mount<{ amount: string }>( 125 | { 127 | fn(values.amount); 128 | }} 129 | > 130 | 131 | , 132 | ); 133 | await waitForComponentToPaint(wrapper); 134 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe(''); 135 | act(() => { 136 | wrapper.find('input#amount').simulate('change', { 137 | target: { 138 | value: '-55.33', 139 | }, 140 | }); 141 | }); 142 | await waitForComponentToPaint(wrapper, 300); 143 | expect(String(wrapper.find('input#amount').at(0).props().value).substring(0, 1)).toBe('¥'); 144 | act(() => { 145 | wrapper.find('button.ant-btn-primary').simulate('click'); 146 | }); 147 | await waitForComponentToPaint(wrapper, 300); 148 | expect(fn).toHaveBeenCalledWith(-55.33); 149 | expect(wrapper.render()).toMatchSnapshot(); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /tests/layout/__snapshots__/footer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DefaultFooter test copyright support false 1`] = ` 4 | 56 | `; 57 | -------------------------------------------------------------------------------- /tests/layout/__snapshots__/waterMark.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`WaterMark test image watermark 1`] = ` 4 | 8 |
16 |
23 | 123 24 |
25 |
42 |
43 | 44 | `; 45 | 46 | exports[`WaterMark test text watermark 1`] = ` 47 | 50 |
58 |
65 |
82 |
83 | 84 | `; 85 | -------------------------------------------------------------------------------- /tests/layout/compatible.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | 3 | import React from 'react'; 4 | import BasicLayout from '@ant-design/pro-layout'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { waitForComponentToPaint } from '../util'; 7 | 8 | it('🐲 layout=sidemenu', async () => { 9 | // @ts-expect-error 10 | const wrapper = mount(); 11 | await waitForComponentToPaint(wrapper); 12 | const menu = wrapper.find('.ant-pro-sider-menu'); 13 | expect(menu.exists()).toBe(true); 14 | act(() => { 15 | wrapper.unmount(); 16 | }); 17 | }); 18 | 19 | it('🐲 layout=topmenu', async () => { 20 | // @ts-expect-error 21 | const wrapper = mount(); 22 | await waitForComponentToPaint(wrapper); 23 | const menu = wrapper.find('.ant-pro-sider-menu'); 24 | expect(menu.exists()).toBe(false); 25 | act(() => { 26 | wrapper.unmount(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/layout/defaultProps.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | route: { 3 | path: '/', 4 | routes: [ 5 | { 6 | path: '/', 7 | name: 'welcome', 8 | routes: [ 9 | { 10 | path: '/welcome', 11 | name: 'one', 12 | routes: [ 13 | { 14 | path: '/welcome/welcome', 15 | name: 'two', 16 | exact: true, 17 | }, 18 | ], 19 | }, 20 | ], 21 | }, 22 | ], 23 | }, 24 | location: { 25 | pathname: '/', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /tests/layout/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import type { MenuTheme } from 'antd/es/menu/MenuContext'; 2 | 3 | export type ContentWidth = 'Fluid' | 'Fixed'; 4 | 5 | export type RenderSetting = { 6 | /** @name false 时不展示顶栏 */ 7 | headerRender?: false; 8 | /** @name false 时不展示页脚 */ 9 | footerRender?: false; 10 | /** @name false 时不展示菜单 */ 11 | menuRender?: false; 12 | /** @name false 时不展示菜单顶栏 */ 13 | menuHeaderRender?: false; 14 | }; 15 | export type PureSettings = { 16 | /** 17 | * @name theme for nav menu 18 | * @name 导航菜单的主题 19 | */ 20 | navTheme: MenuTheme | 'realDark' | undefined; 21 | /** 22 | * Side 为正常模式,top菜单显示在顶部,mix 两种兼有 23 | * 24 | * @name nav menu position: `side` or `top` 25 | * @name 导航菜单的位置 26 | */ 27 | layout: 'side' | 'top' | 'mix'; 28 | /** Layout of content: `Fluid` or `Fixed`, only works when layout is top */ 29 | contentWidth: ContentWidth; 30 | 31 | /** Sticky header */ 32 | fixedHeader: boolean; 33 | /** Sticky siderbar */ 34 | fixSiderbar: boolean; 35 | menu: { locale?: boolean; defaultOpenAll?: boolean; ignoreFlatMenu?: boolean }; 36 | title: string; 37 | // Your custom iconfont Symbol script Url 38 | // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js 39 | // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理 40 | // Usage: https://github.com/ant-design/ant-design-pro/pull/3517 41 | iconfontUrl: string; 42 | primaryColor: string; 43 | colorWeak?: boolean; 44 | splitMenus?: boolean; 45 | }; 46 | 47 | export type ProSettings = PureSettings & RenderSetting; 48 | 49 | const defaultSettings: ProSettings = { 50 | navTheme: 'dark', 51 | layout: 'side', 52 | contentWidth: 'Fluid', 53 | fixedHeader: false, 54 | fixSiderbar: false, 55 | menu: { 56 | locale: true, 57 | }, 58 | title: 'Ant Design Pro', 59 | iconfontUrl: '', 60 | primaryColor: '#1890ff', 61 | }; 62 | 63 | export default defaultSettings; 64 | -------------------------------------------------------------------------------- /tests/layout/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('layout'); 4 | -------------------------------------------------------------------------------- /tests/layout/footer.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount, render } from 'enzyme'; 3 | import { DefaultFooter } from '@ant-design/pro-layout'; 4 | 5 | describe('DefaultFooter test', () => { 6 | it('set title', () => { 7 | const wrapper = mount(); 8 | expect(wrapper.find('.ant-pro-global-footer-links').exists()).toBeFalsy(); 9 | }); 10 | 11 | it('copyright support false', () => { 12 | const wrapper = render(); 13 | expect(wrapper).toMatchSnapshot(); 14 | }); 15 | 16 | it('if copyright and links falsy both, should not to render nothing', () => { 17 | const wrapper = mount(); 18 | expect(wrapper.find('.ant-pro-global-footer').exists()).toBeFalsy(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/layout/getPageTitle.test.tsx: -------------------------------------------------------------------------------- 1 | import { getPageTitle } from '@ant-design/pro-layout'; 2 | 3 | const pageProps = { 4 | pathname: '/welcome', 5 | location: { pathname: '/welcome' }, 6 | logo: 'https://gw.alipayobjects.com/zos/antfincdn/PmY%24TNNDBI/logo.svg', 7 | navTheme: 'dark', 8 | layout: 'side', 9 | contentWidth: 'Fluid', 10 | fixedHeader: false, 11 | fixSiderbar: false, 12 | menu: { locale: true }, 13 | headerHeight: 48, 14 | title: 'Ant Design Pro', 15 | iconfontUrl: '', 16 | primaryColor: '#1890ff', 17 | prefixCls: 'ant-pro', 18 | siderWidth: 208, 19 | breadcrumb: { 20 | '/welcome/welcome': { 21 | path: '/welcome/welcome', 22 | name: 'two', 23 | locale: 'menu.welcome.one.two', 24 | key: '/welcome/welcome', 25 | routes: null, 26 | exact: true, 27 | pro_layout_parentKeys: [ 28 | '/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925', 29 | '/welcome', 30 | ], 31 | }, 32 | '/welcome': { 33 | path: '/welcome', 34 | name: 'one', 35 | locale: 'menu.welcome.one', 36 | key: '/welcome', 37 | routes: null, 38 | children: [ 39 | { 40 | path: '/welcome/welcome', 41 | name: 'two', 42 | locale: 'menu.welcome.one.two', 43 | key: '/welcome/welcome', 44 | routes: null, 45 | exact: true, 46 | pro_layout_parentKeys: [ 47 | '/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925', 48 | '/welcome', 49 | ], 50 | }, 51 | ], 52 | pro_layout_parentKeys: ['/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925'], 53 | }, 54 | '/': { 55 | path: '/', 56 | name: 'welcome', 57 | children: [ 58 | { 59 | path: '/welcome', 60 | name: 'one', 61 | locale: 'menu.welcome.one', 62 | key: '/welcome', 63 | routes: null, 64 | children: [ 65 | { 66 | path: '/welcome/welcome', 67 | name: 'two', 68 | locale: 'menu.welcome.one.two', 69 | key: '/welcome/welcome', 70 | routes: null, 71 | exact: true, 72 | pro_layout_parentKeys: [ 73 | '/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925', 74 | '/welcome', 75 | ], 76 | }, 77 | ], 78 | pro_layout_parentKeys: [ 79 | '/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925', 80 | ], 81 | }, 82 | ], 83 | locale: 'menu.welcome', 84 | key: '/564f79ec010d02670f2cd38274f84017d6ddf17759857629a1399aed6bb20925', 85 | routes: null, 86 | pro_layout_parentKeys: [], 87 | }, 88 | '/demo': { 89 | path: '/demo', 90 | name: 'demo', 91 | locale: 'menu.demo', 92 | key: '/demo', 93 | routes: null, 94 | pro_layout_parentKeys: [], 95 | }, 96 | }, 97 | breadcrumbMap: new Map(), 98 | }; 99 | 100 | describe('getPageTitle', () => { 101 | it('base', () => { 102 | const title = getPageTitle(pageProps); 103 | expect(title).toBe('one - Ant Design Pro'); 104 | }); 105 | 106 | it('base ignoreTitle', () => { 107 | const title = getPageTitle(pageProps, true); 108 | expect(title).toBe('one'); 109 | }); 110 | 111 | it('title=false', () => { 112 | const title = getPageTitle({ 113 | ...pageProps, 114 | title: false, 115 | }); 116 | expect(title).toBe('one'); 117 | }); 118 | 119 | it('base ignoreTitle', () => { 120 | const title = getPageTitle({ ...pageProps, pathname: undefined }, true); 121 | expect(title).toBe('welcome'); 122 | }); 123 | 124 | it('base title=Ant', () => { 125 | const title = getPageTitle({ ...pageProps, title: 'Ant' }); 126 | expect(title).toBe('one - Ant'); 127 | }); 128 | 129 | it('base menu=undefined', () => { 130 | const title = getPageTitle({ ...pageProps, menu: undefined, title: 'Ant' }); 131 | expect(title).toBe('one - Ant'); 132 | }); 133 | 134 | it('title is null ', () => { 135 | const title = getPageTitle({ 136 | ...pageProps, 137 | title: undefined, 138 | }); 139 | expect(title).toBe('one - Ant Design Pro'); 140 | }); 141 | 142 | it('breadcrumb is null ', () => { 143 | const title = getPageTitle({ 144 | ...pageProps, 145 | breadcrumb: {}, 146 | }); 147 | expect(title).toBe('Ant Design Pro'); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /tests/layout/mobile.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount, render } from 'enzyme'; 2 | import React from 'react'; 3 | import BasicLayout from '@ant-design/pro-layout'; 4 | import { act } from 'react-dom/test-utils'; 5 | 6 | import defaultProps from './defaultProps'; 7 | import { waitForComponentToPaint } from '../util'; 8 | 9 | describe('mobile BasicLayout', () => { 10 | beforeAll(() => { 11 | process.env.NODE_ENV = 'TEST'; 12 | process.env.USE_MEDIA = 'xs'; 13 | 14 | Object.defineProperty(global.window, 'matchMedia', { 15 | value: jest.fn((query) => { 16 | // (max-width: 575px) 17 | return { 18 | media: query, 19 | matches: query.includes('max-width: 575px'), 20 | addListener: jest.fn(), 21 | removeListener: jest.fn(), 22 | }; 23 | }), 24 | }); 25 | }); 26 | 27 | afterAll(() => { 28 | process.env.USE_MEDIA = 'md'; 29 | process.env.NODE_ENV = 'dev'; 30 | }); 31 | 32 | it('📱 base use', async () => { 33 | const html = render( 34 | {}} />, 35 | ); 36 | expect(html).toMatchSnapshot(); 37 | }); 38 | 39 | it('📱 collapsed=false', async () => { 40 | const html = render(); 41 | expect(html).toMatchSnapshot(); 42 | }); 43 | 44 | it('📱 layout=mix', async () => { 45 | const html = render( 46 | , 47 | ); 48 | expect(html).toMatchSnapshot(); 49 | }); 50 | 51 | it('📱 layout=mix and splitMenus', async () => { 52 | const html = render( 53 | , 60 | ); 61 | expect(html).toMatchSnapshot(); 62 | }); 63 | 64 | it('📱 layout menuHeaderRender=false', async () => { 65 | const html = render( 66 | , 73 | ); 74 | expect(html).toMatchSnapshot(); 75 | }); 76 | 77 | it('📱 layout menuHeaderRender', async () => { 78 | const html = render( 79 | 'title'} 85 | />, 86 | ); 87 | expect(html).toMatchSnapshot(); 88 | }); 89 | 90 | it('📱 layout menuHeaderRender', async () => { 91 | const html = render( 92 | 'title'} 98 | />, 99 | ); 100 | expect(html).toMatchSnapshot(); 101 | }); 102 | 103 | it('📱 layout collapsedButtonRender', async () => { 104 | const onCollapse = jest.fn(); 105 | const html = mount( 106 | { 111 | return 'div'; 112 | }} 113 | getContainer={false} 114 | layout="mix" 115 | />, 116 | ); 117 | 118 | waitForComponentToPaint(html); 119 | act(() => { 120 | html.find('span.ant-pro-global-header-collapsed-button').simulate('click'); 121 | }); 122 | waitForComponentToPaint(html); 123 | act(() => { 124 | html.find('div.ant-drawer-mask').simulate('click'); 125 | }); 126 | waitForComponentToPaint(html); 127 | expect(onCollapse).toHaveBeenCalled(); 128 | 129 | waitForComponentToPaint(html); 130 | act(() => { 131 | html.unmount(); 132 | }); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /tests/layout/pageHeaderWarp.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, mount } from 'enzyme'; 2 | import React from 'react'; 3 | import ProLayout, { PageContainer } from '@ant-design/pro-layout'; 4 | import { act } from 'react-dom/test-utils'; 5 | import defaultProps from './defaultProps'; 6 | import { waitForComponentToPaint } from '../util'; 7 | 8 | describe('BasicLayout', () => { 9 | it('base use', () => { 10 | const html = render( 11 | 12 | 13 | , 14 | ); 15 | expect(html).toMatchSnapshot(); 16 | }); 17 | 18 | it('content is text', () => { 19 | const html = render( 20 | 21 | 22 | , 23 | ); 24 | expect(html).toMatchSnapshot(); 25 | }); 26 | 27 | it('title=false, don not render title view', async () => { 28 | const wrapper = mount( 29 | 30 | 31 | , 32 | ); 33 | await waitForComponentToPaint(wrapper); 34 | expect(wrapper.find('.ant-page-header-heading-title')).toHaveLength(0); 35 | }); 36 | 37 | it('have default title', async () => { 38 | const wrapper = mount( 39 | 40 | 41 | , 42 | ); 43 | await waitForComponentToPaint(wrapper); 44 | const titleDom = wrapper.find('.ant-page-header-heading-title'); 45 | expect(titleDom.text()).toEqual('welcome'); 46 | }); 47 | 48 | it('title overrides the default title', async () => { 49 | const wrapper = mount( 50 | 51 | 52 | , 53 | ); 54 | await waitForComponentToPaint(wrapper); 55 | const titleDom = wrapper.find('.ant-page-header-heading-title'); 56 | expect(titleDom.text()).toEqual('name'); 57 | }); 58 | 59 | it('with default prefixCls props TopNavHeader', async () => { 60 | const wrapper = mount( 61 | } 67 | > 68 | 69 | , 70 | ); 71 | await waitForComponentToPaint(wrapper); 72 | const domHeader = wrapper.find('.ant-pro-top-nav-header-logo'); 73 | 74 | act(() => { 75 | wrapper.setProps({ 76 | rightContentRender: () => ( 77 |
82 | xx 83 |
84 | ), 85 | }); 86 | }); 87 | expect(domHeader.exists()).toBe(true); 88 | }); 89 | 90 | it('without custom prefixCls props TopNavHeader', async () => { 91 | const prefixCls = 'ant-oh-pro'; 92 | const wrapper = mount( 93 | 94 | 95 | , 96 | ); 97 | await waitForComponentToPaint(wrapper); 98 | const domHeader = wrapper.find(`.${prefixCls}-top-nav-header-logo`); 99 | expect(domHeader.exists()).toBe(true); 100 | }); 101 | 102 | it('pageHeaderRender return false', async () => { 103 | const wrapper = mount( 104 | 105 | null} /> 106 | , 107 | ); 108 | await waitForComponentToPaint(wrapper); 109 | const domHeader = wrapper.find('ant-page-header'); 110 | expect(domHeader.exists()).toBeFalsy(); 111 | act(() => { 112 | wrapper.unmount(); 113 | }); 114 | }); 115 | 116 | it('pageHeaderRender is false', async () => { 117 | const wrapper = mount( 118 | 119 | 120 | , 121 | ); 122 | await waitForComponentToPaint(wrapper); 123 | const domHeader = wrapper.find('ant-page-header'); 124 | expect(domHeader.exists()).toBeFalsy(); 125 | act(() => { 126 | wrapper.unmount(); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /tests/layout/settings.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import BasicLayout from '@ant-design/pro-layout'; 4 | import { waitForComponentToPaint } from '../util'; 5 | import { act } from 'react-dom/test-utils'; 6 | 7 | describe('settings.test', () => { 8 | it('set title', async () => { 9 | const wrapper = mount(); 10 | await waitForComponentToPaint(wrapper); 11 | let title = wrapper.find('#logo').at(0).text(); 12 | expect(title).toEqual('test-title'); 13 | act(() => { 14 | wrapper.setProps({ 15 | title: 'Ant Design Pro', 16 | }); 17 | }); 18 | title = wrapper.find('#logo').text(); 19 | expect(title).toEqual('Ant Design Pro'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/layout/waterMark.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { mount } from 'enzyme'; 3 | import { WaterMark } from '@ant-design/pro-layout'; 4 | import { waitForComponentToPaint } from '../util'; 5 | import { act } from 'react-dom/test-utils'; 6 | 7 | describe('WaterMark', () => { 8 | it('test image watermark', async () => { 9 | let onloadRef: Function | undefined; 10 | 11 | Object.defineProperty(Image.prototype, 'onload', { 12 | get() { 13 | // eslint-disable-next-line no-underscore-dangle 14 | return this._onload; 15 | }, 16 | set(onload: Function) { 17 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 18 | onloadRef = onload; 19 | // eslint-disable-next-line no-underscore-dangle 20 | this._onload = onload; 21 | }, 22 | }); 23 | const wrapper = mount( 24 | 28 |
123
29 |
, 30 | ); 31 | 32 | await waitForComponentToPaint(wrapper, 100); 33 | wrapper.update(); 34 | act(() => { 35 | onloadRef?.(); 36 | }); 37 | expect(wrapper).toMatchSnapshot(); 38 | wrapper.unmount(); 39 | }); 40 | 41 | it('test text watermark', () => { 42 | const wrapper = mount( 43 | 44 |
45 | , 46 | ); 47 | wrapper.update(); 48 | expect(wrapper).toMatchSnapshot(); 49 | wrapper.unmount(); 50 | }); 51 | 52 | it('test image watermark', async () => { 53 | const spy = jest.spyOn(global.console, 'error').mockImplementation(); 54 | const createElement = document.createElement.bind(document); 55 | // @ts-ignore 56 | document.createElement = (tagName: string) => { 57 | if (tagName === 'canvas') { 58 | return { 59 | setAttribute: () => null, 60 | getContext: () => null, 61 | measureText: () => ({}), 62 | }; 63 | } 64 | return createElement(tagName); 65 | }; 66 | 67 | const wrapper = mount( 68 | 72 |
123
73 |
, 74 | ); 75 | 76 | await waitForComponentToPaint(wrapper, 100); 77 | wrapper.update(); 78 | // @ts-ignore 79 | expect(console.error.mock.calls).toEqual([['当前环境不支持Canvas']]); 80 | wrapper.unmount(); 81 | spy.mockRestore(); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tests/list/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`List 🚏 only has content 1`] = ` 4 |
7 |
10 |
14 |
17 |
20 |
23 |
    26 |
    29 |
  • 32 |
    35 |
    38 |
    39 |
    42 |
    43 | 段落示意:蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态,提供跨越设计与开发的体验解决方案。蚂蚁金服设计平台 design.alipay.com,用最小的工作量,无缝接入蚂蚁金服生态提供跨越设计与开发的体验解决方案。 44 |
    45 |
    46 |
  • 47 |
    48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | `; 56 | 57 | exports[`List 🚏 only has description 1`] = ` 58 |
61 |
64 |
68 |
71 |
74 |
77 |
    80 |
    83 |
  • 86 |
    89 |
    92 |
    95 |
    98 |
    101 |
    104 | 107 | 语雀专栏 108 | 109 | 112 | 设计语言 113 | 114 | 117 | 蚂蚁金服 118 | 119 |
    120 |
    121 |
    122 |
    123 |
    124 |
  • 125 |
    126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 | `; 134 | -------------------------------------------------------------------------------- /tests/list/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('list'); 4 | -------------------------------------------------------------------------------- /tests/no-duplicated.ts: -------------------------------------------------------------------------------- 1 | // make sure no duplicated export interface 2 | export * from '../packages/card/src'; 3 | export * from '../packages/descriptions/src'; 4 | export * from '../packages/form/src'; 5 | export * from '../packages/layout/src'; 6 | export * from '../packages/list/src'; 7 | export * from '../packages/skeleton/src'; 8 | export * from '../packages/table/src'; 9 | -------------------------------------------------------------------------------- /tests/setupTests.js: -------------------------------------------------------------------------------- 1 | import MockDate from 'mockdate'; 2 | import Enzyme from 'enzyme'; 3 | import 'jest-canvas-mock'; 4 | import moment from 'moment-timezone'; 5 | 6 | import { enableFetchMocks } from 'jest-fetch-mock'; 7 | import tableData from './table/mock.data.json'; 8 | 9 | jest.mock('react', () => ({ 10 | ...jest.requireActual('react'), 11 | useLayoutEffect: jest.requireActual('react').useEffect, 12 | })); 13 | 14 | const eventListener = {}; 15 | /* eslint-disable global-require */ 16 | if (typeof window !== 'undefined') { 17 | global.window.resizeTo = (width, height) => { 18 | global.window.innerWidth = width || global.window.innerWidth; 19 | global.window.innerHeight = height || global.window.innerHeight; 20 | global.window.dispatchEvent(new Event('resize')); 21 | }; 22 | document.addEventListener = (name, cb) => { 23 | eventListener[name] = cb; 24 | }; 25 | document.dispatchEvent = (event) => eventListener[event.type]?.(event); 26 | global.window.scrollTo = () => {}; 27 | // ref: https://github.com/ant-design/ant-design/issues/18774 28 | if (!window.matchMedia) { 29 | Object.defineProperty(global.window, 'matchMedia', { 30 | writable: true, 31 | configurable: true, 32 | value: jest.fn(() => ({ 33 | matches: false, 34 | addListener: jest.fn(), 35 | removeListener: jest.fn(), 36 | })), 37 | }); 38 | } 39 | if (!window.matchMedia) { 40 | Object.defineProperty(global.window, 'matchMedia', { 41 | writable: true, 42 | configurable: true, 43 | value: jest.fn((query) => ({ 44 | matches: query.includes('max-width'), 45 | addListener: jest.fn(), 46 | removeListener: jest.fn(), 47 | })), 48 | }); 49 | } 50 | } 51 | 52 | Object.assign(Enzyme.ReactWrapper.prototype, { 53 | findObserver() { 54 | return this.find('ResizeObserver'); 55 | }, 56 | triggerResize() { 57 | const ob = this.findObserver(); 58 | ob.instance().onResize([{ target: ob.getDOMNode() }]); 59 | }, 60 | }); 61 | 62 | enableFetchMocks(); 63 | 64 | global.requestAnimationFrame = 65 | global.requestAnimationFrame || 66 | function requestAnimationFrame(cb) { 67 | return setTimeout(cb, 0); 68 | }; 69 | 70 | global.cancelAnimationFrame = 71 | global.cancelAnimationFrame || 72 | function cancelAnimationFrame() { 73 | return null; 74 | }; 75 | // browserMocks.js 76 | const localStorageMock = (() => { 77 | let store = { 78 | umi_locale: 'zh-CN', 79 | }; 80 | 81 | return { 82 | getItem(key) { 83 | return store[key] || null; 84 | }, 85 | setItem(key, value) { 86 | store[key] = value.toString(); 87 | }, 88 | removeItem(key) { 89 | store[key] = null; 90 | }, 91 | clear() { 92 | store = {}; 93 | }, 94 | }; 95 | })(); 96 | 97 | Object.defineProperty(window, 'localStorage', { 98 | value: localStorageMock, 99 | }); 100 | 101 | Object.defineProperty(window, 'cancelAnimationFrame', { 102 | value: () => null, 103 | }); 104 | 105 | moment.tz.setDefault('UTC'); 106 | 107 | // 2016-11-22 15:22:44 108 | MockDate.set(1479799364000); 109 | 110 | const mockFormatExpression = { 111 | format: (value) => `¥ ${value.toString()}`, 112 | }; 113 | Intl.NumberFormat = jest.fn().mockImplementation(() => mockFormatExpression); 114 | 115 | Math.random = () => 0.8404419276253765; 116 | 117 | fetch.mockResponse(async () => { 118 | return { body: JSON.stringify(tableData) }; 119 | }); 120 | 121 | Object.assign(Enzyme.ReactWrapper.prototype, { 122 | findObserver() { 123 | return this.find('ResizeObserver'); 124 | }, 125 | triggerResize() { 126 | const ob = this.findObserver(); 127 | ob.instance().onResize([{ target: ob.getDOMNode() }]); 128 | }, 129 | }); 130 | 131 | // @ts-ignore-next-line 132 | global.Worker = class { 133 | constructor(stringUrl) { 134 | // @ts-ignore-next-line 135 | this.url = stringUrl; 136 | // @ts-ignore-next-line 137 | this.onmessage = () => {}; 138 | } 139 | 140 | postMessage(msg) { 141 | // @ts-ignore-next-line 142 | this.onmessage(msg); 143 | } 144 | }; 145 | 146 | // @ts-ignore-next-line 147 | global.URL.createObjectURL = () => {}; 148 | -------------------------------------------------------------------------------- /tests/skeleton/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('skeleton'); 4 | -------------------------------------------------------------------------------- /tests/skeleton/skeleton.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { act } from 'react-dom/test-utils'; 4 | 5 | import ProSkeleton from '../../packages/skeleton/src/index'; 6 | 7 | describe('skeleton', () => { 8 | it('🥩 list base use', async () => { 9 | const html = render(); 10 | expect(html).toMatchSnapshot(); 11 | }); 12 | 13 | it('🥩 descriptions base use', async () => { 14 | const html = render(); 15 | expect(html).toMatchSnapshot(); 16 | }); 17 | 18 | it('🥩 result base use', async () => { 19 | const html = render(); 20 | expect(html).toMatchSnapshot(); 21 | }); 22 | 23 | it('🥩 descriptions api use', async () => { 24 | const wrapper = mount(); 25 | expect(wrapper.render()).toMatchSnapshot(); 26 | act(() => { 27 | wrapper.setProps({ 28 | table: false, 29 | }); 30 | }); 31 | expect(wrapper.render()).toMatchSnapshot(); 32 | }); 33 | 34 | it('🥩 list api use', async () => { 35 | const wrapper = mount( 36 | , 44 | ); 45 | expect(wrapper.render()).toMatchSnapshot(); 46 | act(() => { 47 | wrapper.setProps({ 48 | list: false, 49 | statistic: false, 50 | }); 51 | }); 52 | expect(wrapper.render()).toMatchSnapshot(); 53 | }); 54 | 55 | it('🥩 statistic=1,span=16', async () => { 56 | const wrapper = mount( 57 | , 65 | ); 66 | expect(wrapper.render()).toMatchSnapshot(); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /tests/table/__snapshots__/listtoolbar.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Table valueEnum ListToolBar action is empty array 1`] = ` 4 |
7 |
10 |
13 |
16 |
17 |
18 | `; 19 | 20 | exports[`Table valueEnum ListToolBar action no array 1`] = ` 21 |
24 |
27 |
30 |
33 |
36 | 44 |
45 |
46 |
47 |
48 | `; 49 | 50 | exports[`Table valueEnum ListToolBar action no jsx 1`] = ` 51 |
54 |
57 |
60 |
63 |
66 |
69 |
73 | 81 |
82 |
85 | shuaxin 86 |
87 |
88 |
89 |
90 |
91 |
92 | `; 93 | -------------------------------------------------------------------------------- /tests/table/__snapshots__/search.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BasicTable Search 🎏 request load more time 1`] = `null`; 4 | -------------------------------------------------------------------------------- /tests/table/column.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { ConfigProvider } from 'antd'; 4 | import ProTable from '@ant-design/pro-table'; 5 | import { request } from './demo'; 6 | import { waitForComponentToPaint } from '../util'; 7 | import moment from 'moment'; 8 | import { MenuOutlined } from '@ant-design/icons'; 9 | 10 | describe('Table ColumnSetting', () => { 11 | it('🎏 render', async () => { 12 | const callBack = jest.fn(); 13 | const html = mount( 14 | callBack(text), 22 | }, 23 | ]} 24 | request={request} 25 | rowKey="key" 26 | />, 27 | ); 28 | await waitForComponentToPaint(html, 1200); 29 | expect(callBack).toBeCalled(); 30 | expect(callBack).toBeCalledWith('Edward King 0'); 31 | }); 32 | 33 | it('🎏 query should parse by valueType', async () => { 34 | const callBack = jest.fn(); 35 | const html = mount( 36 | { 52 | console.log(params); 53 | callBack(params.date); 54 | return { 55 | data: [ 56 | { 57 | key: '1', 58 | date: moment(), 59 | }, 60 | ], 61 | success: true, 62 | }; 63 | }} 64 | rowKey="key" 65 | />, 66 | ); 67 | await waitForComponentToPaint(html, 1000); 68 | expect(callBack).toBeCalled(); 69 | expect(callBack).toBeCalledWith('2016-11-22'); 70 | }); 71 | 72 | it('🎏 config provide render', async () => { 73 | const html = mount( 74 | 75 | 87 | , 88 | ); 89 | await waitForComponentToPaint(html, 1200); 90 | expect(html.render()).toMatchSnapshot(); 91 | }); 92 | 93 | it('🎏 render text', async () => { 94 | const callBack = jest.fn(); 95 | const html = mount( 96 | callBack(text), 104 | }, 105 | { 106 | title: 'Name2', 107 | key: 'name2', 108 | dataIndex: 'name2', 109 | valueType: false, 110 | }, 111 | ]} 112 | request={request} 113 | rowKey="key" 114 | />, 115 | ); 116 | await waitForComponentToPaint(html, 1200); 117 | expect(callBack).toBeCalled(); 118 | expect(callBack).toBeCalledWith('Edward King 0'); 119 | }); 120 | 121 | it('🎏 change text by renderText', async () => { 122 | const html = mount( 123 | `${text}2144`, 131 | }, 132 | ]} 133 | search={false} 134 | request={request} 135 | rowKey="key" 136 | />, 137 | ); 138 | await waitForComponentToPaint(html, 1200); 139 | expect(html.find('td.ant-table-cell')).toMatchSnapshot(); 140 | }); 141 | }); 142 | -------------------------------------------------------------------------------- /tests/table/demo.test.ts: -------------------------------------------------------------------------------- 1 | import demoTest from '../demo'; 2 | 3 | demoTest('table'); 4 | -------------------------------------------------------------------------------- /tests/table/demo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import moment from 'moment'; 3 | import type { ProColumns } from '@ant-design/pro-table'; 4 | import { TableStatus, TableDropdown } from '@ant-design/pro-table'; 5 | import { Input, message } from 'antd'; 6 | 7 | const getFetchData = ( 8 | size: number, 9 | ): { 10 | key: string | number; 11 | name: string; 12 | age: string | number; 13 | address: string; 14 | money: number; 15 | sex: string; 16 | date: number; 17 | status: number; 18 | }[] => { 19 | const data: { 20 | key: string | number; 21 | name: string; 22 | age: string | number; 23 | address: string; 24 | money: number; 25 | sex: string; 26 | date: number; 27 | status: number; 28 | }[] = []; 29 | 30 | for (let i = 0; i < size; i += 1) { 31 | data.push({ 32 | key: `${i}`, 33 | name: `Edward King ${i}`, 34 | age: 10 + i, 35 | status: Math.floor(i) % 4, 36 | sex: i / 2 > 1 ? 'man' : 'woman', 37 | money: parseFloat((10000.26 * (i + 1)).toFixed(2)), 38 | date: moment('2019-11-16 12:50:26').valueOf() + i * 1000 * 60 * 2, 39 | address: `London, Park Lane no. ${i}`, 40 | }); 41 | } 42 | return data; 43 | }; 44 | 45 | export const columns: ProColumns[] = [ 46 | { 47 | title: '序号', 48 | key: 'index', 49 | dataIndex: 'index', 50 | valueType: 'index', 51 | }, 52 | { 53 | title: '边框序号', 54 | dataIndex: 'indexBorder', 55 | valueType: 'indexBorder', 56 | }, 57 | { 58 | title: 'Name', 59 | key: 'name', 60 | dataIndex: 'name', 61 | copyable: true, 62 | }, 63 | { 64 | title: 'Textarea', 65 | key: 'textarea', 66 | dataIndex: 'name', 67 | valueType: 'textarea', 68 | copyable: true, 69 | }, 70 | { 71 | title: 'sex', 72 | dataIndex: 'sex', 73 | key: 'sex', 74 | filters: true, 75 | onFilter: true, 76 | valueType: 'select', 77 | valueEnum: { 78 | man: '男', 79 | woman: '女', 80 | }, 81 | }, 82 | { 83 | title: '状态', 84 | dataIndex: 'status', 85 | hideInForm: true, 86 | valueType: 'select', 87 | valueEnum: { 88 | 0: { text: '关闭', status: 'Default' }, 89 | 1: { text: '运行中', status: 'Processing' }, 90 | 2: { text: '已上线', status: 'Success' }, 91 | 3: { text: '异常', status: 'Error' }, 92 | }, 93 | }, 94 | { 95 | title: 'Age', 96 | key: 'age', 97 | dataIndex: 'age', 98 | }, 99 | { 100 | title: 'Address', 101 | dataIndex: 'address', 102 | ellipsis: true, 103 | width: 100, 104 | }, 105 | { 106 | title: 'money', 107 | dataIndex: 'money', 108 | valueType: 'money', 109 | }, 110 | { 111 | title: 'date', 112 | key: 'date', 113 | dataIndex: 'date', 114 | valueType: 'date', 115 | renderFormItem: () => , 116 | }, 117 | { 118 | title: 'dateTime', 119 | key: 'dateTime', 120 | dataIndex: 'date', 121 | valueType: 'dateTime', 122 | }, 123 | { 124 | title: 'time', 125 | key: 'time', 126 | dataIndex: 'date', 127 | valueType: 'time', 128 | renderText: () => moment('2019-11-16 12:50:26'), 129 | }, 130 | { 131 | title: '状态', 132 | dataIndex: 'status2', 133 | render: () => ( 134 |
135 | 上线成功 136 |
137 | 上线失败 138 |
139 | 正在部署 140 |
141 | 正在初始化 142 |
143 | ), 144 | }, 145 | { 146 | title: 'option', 147 | valueType: 'option', 148 | key: 'option', 149 | dataIndex: 'id', 150 | render: (text, row, index, action) => [ 151 | { 154 | message.info('确认删除'); 155 | action?.reload(); 156 | }} 157 | > 158 | delete 159 | , 160 | { 163 | message.info('确认刷新'); 164 | action?.reload(); 165 | }} 166 | > 167 | reload 168 | , 169 | message.info(key)} 172 | menus={[ 173 | { key: 'copy', name: '复制' }, 174 | { key: 'delete', name: '删除' }, 175 | ]} 176 | />, 177 | ], 178 | }, 179 | ]; 180 | 181 | export { getFetchData }; 182 | 183 | export const request = (params?: { 184 | pageSize?: number | undefined; 185 | current?: number | undefined; 186 | }): Promise<{ 187 | data: { 188 | key: string | number; 189 | name: string; 190 | age: string | number; 191 | address: string; 192 | }[]; 193 | success: true; 194 | }> => 195 | Promise.resolve({ 196 | data: getFetchData(params?.pageSize || 46), 197 | total: 200, 198 | success: true, 199 | }); 200 | -------------------------------------------------------------------------------- /tests/table/polling.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { act } from 'react-dom/test-utils'; 4 | import ProTable from '@ant-design/pro-table'; 5 | import { columns } from './demo'; 6 | import { waitForComponentToPaint } from '../util'; 7 | 8 | describe('polling', () => { 9 | it('⏱️ polling should clearTime', async () => { 10 | const fn = jest.fn(); 11 | const html = mount( 12 | { 18 | fn(); 19 | return Promise.resolve({ 20 | data: [], 21 | total: 20, 22 | success: true, 23 | }); 24 | }} 25 | rowKey="key" 26 | />, 27 | ); 28 | await waitForComponentToPaint(html, 1000); 29 | expect(fn).toBeCalledTimes(1); 30 | 31 | await waitForComponentToPaint(html, 2000); 32 | 33 | expect(fn).toBeCalledTimes(2); 34 | 35 | act(() => { 36 | html.unmount(); 37 | }); 38 | await waitForComponentToPaint(html, 2000); 39 | 40 | expect(fn).toBeCalledTimes(2); 41 | }); 42 | 43 | it('⏱️ polling min time is 2000', async () => { 44 | const fn = jest.fn(); 45 | const html = mount( 46 | { 52 | fn(); 53 | return Promise.resolve({ 54 | data: [], 55 | total: 20, 56 | success: true, 57 | }); 58 | }} 59 | rowKey="key" 60 | />, 61 | ); 62 | await waitForComponentToPaint(html, 1000); 63 | expect(fn).toBeCalledTimes(1); 64 | 65 | await waitForComponentToPaint(html, 2000); 66 | 67 | expect(fn).toBeCalledTimes(2); 68 | }); 69 | 70 | it('⏱️ polling time=3000', async () => { 71 | const fn = jest.fn(); 72 | const html = mount( 73 | { 79 | fn(); 80 | return Promise.resolve({ 81 | data: [], 82 | total: 20, 83 | success: true, 84 | }); 85 | }} 86 | rowKey="key" 87 | />, 88 | ); 89 | await waitForComponentToPaint(html, 1000); 90 | expect(fn).toBeCalledTimes(1); 91 | 92 | await waitForComponentToPaint(html, 1000); 93 | 94 | expect(fn).toBeCalledTimes(1); 95 | 96 | await waitForComponentToPaint(html, 2000); 97 | expect(fn).toBeCalledTimes(2); 98 | }); 99 | 100 | it('⏱️ polling support function', async () => { 101 | const fn = jest.fn(); 102 | const html = mount( 103 | { 108 | return 2000; 109 | }} 110 | request={async () => { 111 | fn(); 112 | return Promise.resolve({ 113 | data: [], 114 | total: 20, 115 | success: true, 116 | }); 117 | }} 118 | rowKey="key" 119 | />, 120 | ); 121 | await waitForComponentToPaint(html, 1000); 122 | expect(fn).toBeCalledTimes(1); 123 | 124 | await waitForComponentToPaint(html, 2000); 125 | 126 | expect(fn).toBeCalledTimes(2); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /tests/table/selectKeys.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import { act } from 'react-dom/test-utils'; 4 | import ProTable from '@ant-design/pro-table'; 5 | import { getFetchData } from './demo'; 6 | import { waitForComponentToPaint } from '../util'; 7 | 8 | describe('BasicTable Search', () => { 9 | const LINE_STR_COUNT = 20; 10 | // Mock offsetHeight 11 | // @ts-expect-error 12 | const originOffsetHeight = Object.getOwnPropertyDescriptor( 13 | HTMLElement.prototype, 14 | 'offsetHeight', 15 | ).get; 16 | Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { 17 | get() { 18 | let html = this.innerHTML; 19 | html = html.replace(/<[^>]*>/g, ''); 20 | const lines = Math.ceil(html.length / LINE_STR_COUNT); 21 | return lines * 16; 22 | }, 23 | }); 24 | 25 | // Mock getComputedStyle 26 | const originGetComputedStyle = window.getComputedStyle; 27 | window.getComputedStyle = (ele) => { 28 | const style = originGetComputedStyle(ele); 29 | style.lineHeight = '16px'; 30 | return style; 31 | }; 32 | 33 | afterAll(() => { 34 | Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { 35 | get: originOffsetHeight, 36 | }); 37 | window.getComputedStyle = originGetComputedStyle; 38 | }); 39 | 40 | it('🎏 filter test', async () => { 41 | const fn = jest.fn(); 42 | const html = mount( 43 | , 70 | ); 71 | await waitForComponentToPaint(html, 200); 72 | act(() => { 73 | html 74 | .find('.ant-table-cell label.ant-checkbox-wrapper input') 75 | .at(1) 76 | .simulate('change', { 77 | target: { 78 | checked: true, 79 | }, 80 | }); 81 | }); 82 | await waitForComponentToPaint(html, 200); 83 | expect(fn).toBeCalledTimes(1); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/table/valueEnum.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React, { useContext } from 'react'; 3 | import ProProvider from '@ant-design/pro-provider'; 4 | import ProTable from '@ant-design/pro-table'; 5 | import { act } from 'react-dom/test-utils'; 6 | import { Input } from 'antd'; 7 | 8 | import { waitForComponentToPaint } from '../util'; 9 | 10 | const Demo = () => { 11 | const values = useContext(ProProvider); 12 | return ( 13 | {text}, 19 | renderFormItem: (text, props) => ( 20 | 21 | ), 22 | }, 23 | }, 24 | }} 25 | > 26 | , 'link' | 'tags'> 27 | columns={[ 28 | { 29 | title: '链接', 30 | dataIndex: 'name', 31 | valueType: 'link', 32 | }, 33 | ]} 34 | request={() => { 35 | return Promise.resolve({ 36 | total: 200, 37 | data: [ 38 | { 39 | key: 1, 40 | name: 'test', 41 | }, 42 | ], 43 | success: true, 44 | }); 45 | }} 46 | rowKey="key" 47 | /> 48 | 49 | ); 50 | }; 51 | 52 | describe('Table valueEnum', () => { 53 | it('🎏 dynamic enum test', async () => { 54 | const html = mount( 55 | ({ 68 | data: [ 69 | { 70 | status: 2, 71 | key: '1', 72 | }, 73 | ], 74 | })} 75 | rowKey="key" 76 | />, 77 | ); 78 | await waitForComponentToPaint(html, 1200); 79 | 80 | act(() => { 81 | html.setProps({ 82 | columns: [ 83 | { 84 | title: '状态', 85 | dataIndex: 'status', 86 | valueEnum: { 87 | 0: { text: '关闭', status: 'Default' }, 88 | 1: { text: '运行中', status: 'Processing', disabled: true }, 89 | 2: { text: '已上线', status: 'Success' }, 90 | 3: { text: '异常', status: 'Error' }, 91 | }, 92 | fieldProps: { 93 | open: true, 94 | }, 95 | }, 96 | ], 97 | }); 98 | }); 99 | await waitForComponentToPaint(html, 200); 100 | act(() => { 101 | html.find('form.ant-form div.ant-select').simulate('click'); 102 | }); 103 | act(() => { 104 | expect(html.find('div.ant-select-dropdown').render()).toMatchSnapshot(); 105 | }); 106 | expect(html.find('td.ant-table-cell').text()).toBe('已上线'); 107 | }); 108 | 109 | it('🎏 customization valueType', async () => { 110 | const html = mount(); 111 | await waitForComponentToPaint(html, 1200); 112 | act(() => { 113 | expect(html.render()).toMatchSnapshot(); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/table/valueType.test.tsx: -------------------------------------------------------------------------------- 1 | import { mount } from 'enzyme'; 2 | import React from 'react'; 3 | import ProTable from '@ant-design/pro-table'; 4 | import { Input } from 'antd'; 5 | import ProProvider from '@ant-design/pro-provider'; 6 | 7 | import { waitForComponentToPaint } from '../util'; 8 | import { act } from 'react-dom/test-utils'; 9 | 10 | const defaultProps = { 11 | columns: [ 12 | { 13 | title: '标签', 14 | dataIndex: 'name', 15 | key: 'name', 16 | valueType: 'link', 17 | fieldProps: { 18 | color: 'red', 19 | }, 20 | }, 21 | ], 22 | rowKey: 'key', 23 | request: () => { 24 | return Promise.resolve({ 25 | total: 200, 26 | data: [ 27 | { 28 | key: 0, 29 | name: 'TradeCode 0', 30 | }, 31 | ], 32 | success: true, 33 | }); 34 | }, 35 | }; 36 | 37 | describe('BasicTable valueType', () => { 38 | it('🎏 table support user valueType', async () => { 39 | const html = mount( 40 | {text}, 46 | renderFormItem: (_: any, props: any) => ( 47 | 48 | ), 49 | }, 50 | }, 51 | } as any 52 | } 53 | > 54 | 60 | , 61 | ); 62 | await waitForComponentToPaint(html, 1200); 63 | 64 | expect(html.find('#link').text()).toBe('TradeCode 0'); 65 | 66 | expect(html.find('input#name').exists()).toBeTruthy(); 67 | 68 | expect(html.find('input#name').props().value).toBe('TradeCode'); 69 | 70 | act(() => { 71 | html.unmount(); 72 | }); 73 | }); 74 | 75 | it('🎏 table valueType render support fieldProps', async () => { 76 | const html = mount( 77 | ( 83 | 84 | {text} 85 | {fieldProps.color} 86 | 87 | ), 88 | renderFormItem: (_: any, props: any) => ( 89 | 90 | ), 91 | }, 92 | }, 93 | } as any 94 | } 95 | > 96 | 102 | , 103 | ); 104 | await waitForComponentToPaint(html, 1200); 105 | 106 | expect(html.find('#link').text()).toBe('TradeCode 0red'); 107 | 108 | expect(html.find('input#name').exists()).toBeTruthy(); 109 | 110 | expect(html.find('input#name').props().color).toBe('red'); 111 | 112 | act(() => { 113 | html.unmount(); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/tsconfig.duplicate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "include": ["no-duplicated.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /tests/util.ts: -------------------------------------------------------------------------------- 1 | import { act } from 'react-dom/test-utils'; 2 | 3 | export const waitForComponentToPaint = async (wrapper: any, time = 50) => { 4 | await act(async () => { 5 | wrapper.update?.(); 6 | await new Promise((resolve) => setTimeout(resolve, time)); 7 | wrapper.update?.(); 8 | }); 9 | }; 10 | 11 | export const waitTime = (time: number = 100) => { 12 | return new Promise((resolve) => { 13 | setTimeout(() => { 14 | resolve(true); 15 | }, time); 16 | }); 17 | }; 18 | 19 | export const resizeWindow = (x: number, y: number) => { 20 | // @ts-ignore 21 | window.innerWidth = x; 22 | // @ts-ignore 23 | window.innerHeight = y; 24 | window.dispatchEvent(new Event('resize')); 25 | }; 26 | /* eslint-disable no-param-reassign */ 27 | const NO_EXIST = { __NOT_EXIST: true }; 28 | 29 | export function spyElementPrototypes( 30 | Element: { prototype: Record }, 31 | properties: { [x: string]: any; [x: number]: any }, 32 | ) { 33 | const propNames = Object.keys(properties); 34 | const originDescriptors = {}; 35 | 36 | propNames.forEach((propName) => { 37 | const originDescriptor = Object.getOwnPropertyDescriptor(Element.prototype, propName); 38 | originDescriptors[propName] = originDescriptor || NO_EXIST; 39 | 40 | const spyProp = properties[propName]; 41 | 42 | if (typeof spyProp === 'function') { 43 | // If is a function 44 | Element.prototype[propName] = function spyFunc(...args: any[]) { 45 | return spyProp.call(this, originDescriptor, ...args); 46 | }; 47 | } else { 48 | // Otherwise tread as a property 49 | Object.defineProperty(Element.prototype, propName, { 50 | ...spyProp, 51 | set(value) { 52 | if (spyProp.set) { 53 | return spyProp.set.call(this, originDescriptor, value); 54 | } 55 | return originDescriptor?.set?.(value); 56 | }, 57 | get() { 58 | if (spyProp.get) { 59 | return spyProp.get.call(this, originDescriptor); 60 | } 61 | return originDescriptor?.get?.(); 62 | }, 63 | configurable: true, 64 | }); 65 | } 66 | }); 67 | 68 | return { 69 | mockRestore() { 70 | propNames.forEach((propName) => { 71 | const originDescriptor = originDescriptors[propName]; 72 | if (originDescriptor === NO_EXIST) { 73 | delete Element.prototype[propName]; 74 | } else if (typeof originDescriptor === 'function') { 75 | Element.prototype[propName] = originDescriptor; 76 | } else { 77 | Object.defineProperty(Element.prototype, propName, originDescriptor); 78 | } 79 | }); 80 | }, 81 | }; 82 | } 83 | 84 | export function spyElementPrototype( 85 | Element: { prototype: Record }, 86 | propName: any, 87 | property: any, 88 | ) { 89 | return spyElementPrototypes(Element, { 90 | [propName]: property, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /tests/utils/__snapshots__/index.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`utils LabelIconTip 1`] = ` 4 |
7 | xxx 8 |
11 | xxx 12 |
13 | 16 | 21 | 34 | 35 | 36 |
37 | `; 38 | -------------------------------------------------------------------------------- /theme/builtins/Previewer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LazyLoad from 'react-lazyload'; 3 | import { isBrowser } from 'umi'; 4 | // @ts-ignore 5 | import ProSkeleton from '@ant-design/pro-skeleton'; 6 | import PreView, { IPreviewerProps } from 'dumi-theme-default/src/builtins/Previewer'; 7 | import { Spin } from 'antd'; 8 | 9 | export default ({ 10 | children, 11 | ...rest 12 | }: IPreviewerProps & { 13 | height: string; 14 | }) => { 15 | if (!isBrowser()) { 16 | return null; 17 | } 18 | return ( 19 | 300 ? ( 24 |
31 | 32 |
33 | ) : ( 34 |
35 | 36 |
37 | ) 38 | } 39 | once 40 | > 41 | 42 |
47 | {children} 48 |
49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /theme/layout.less: -------------------------------------------------------------------------------- 1 | .__dumi-default-menu .__dumi-default-menu-list > li > a { 2 | padding-left: 24px !important; 3 | 4 | ~ ul { 5 | margin-left: 24px !important; 6 | } 7 | } 8 | 9 | .markdown table { 10 | max-width: 100vw !important; 11 | } 12 | 13 | html, 14 | .__dumi-default-layout-toc, 15 | .__dumi-default-menu-inner { 16 | &::-webkit-scrollbar { 17 | width: 6px; 18 | height: 6px; 19 | } 20 | &::-webkit-scrollbar-thumb { 21 | background-color: rgba(50, 50, 50, 0.3); 22 | border-radius: 1em; 23 | } 24 | &::-webkit-scrollbar-track { 25 | background-color: rgba(50, 50, 50, 0.1); 26 | border-radius: 1em; 27 | } 28 | } 29 | 30 | .__dumi-default-locale-select { 31 | margin-right: 16px; 32 | margin-left: 16px !important; 33 | } 34 | 35 | html { 36 | &::-webkit-scrollbar { 37 | width: 8px; 38 | height: 6px; 39 | } 40 | } 41 | .__dumi-default-layout-hero { 42 | background-color: #1890ff !important; 43 | background-image: url(https://gw.alipayobjects.com/mdn/rms_08e378/afts/img/A*xOYlR4e8ihIAAAAAAAAAAABkARQnAQ); 44 | background-repeat: no-repeat; 45 | background-position: 90% center; 46 | background-size: contain; 47 | transition: 0.3s all; 48 | 49 | &:hover { 50 | background-position-y: -20px; 51 | } 52 | 53 | h1 { 54 | color: #fff !important; 55 | } 56 | 57 | div > p { 58 | color: #fff; 59 | } 60 | } 61 | 62 | .procomponents_dark_theme_view { 63 | height: 64px; 64 | background-color: #fff; 65 | } 66 | 67 | @media screen and (max-width: 680px) { 68 | .procomponents_dark_theme_view { 69 | height: 50px; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /theme/layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useEffect, useMemo, useState } from 'react'; 2 | import Layout from 'dumi-theme-default/src/layout'; 3 | import dumiContext from '@umijs/preset-dumi/lib/theme/context'; 4 | import { ConfigProvider, Switch } from 'antd'; 5 | import { IRouteComponentProps, isBrowser } from 'umi'; 6 | import zhCN from 'antd/es/locale/zh_CN'; 7 | import { Helmet, HelmetProvider } from 'react-helmet-async'; 8 | import moment from 'moment'; 9 | import useDarkreader from './useDarkreader'; 10 | import 'moment/locale/zh-cn'; 11 | import './layout.less'; 12 | moment.locale('zh-cn'); 13 | 14 | const DarkButton = () => { 15 | const colorScheme = useMemo(() => { 16 | if (!isBrowser()) { 17 | return 'light'; 18 | } 19 | 20 | return matchMedia?.('(prefers-color-scheme: dark)').matches && 'dark'; 21 | }, []); 22 | 23 | const defaultDarken = useMemo(() => { 24 | if (!isBrowser()) { 25 | return 'light'; 26 | } 27 | return localStorage.getItem('procomponents_dark_theme') || colorScheme; 28 | }, []); 29 | 30 | const setColor = (isDarken: boolean) => { 31 | try { 32 | const theme = document.getElementsByTagName('meta')['theme-color']; 33 | theme.setAttribute('content', isDarken ? '#242525' : '#1890ff'); 34 | } catch (error) {} 35 | }; 36 | 37 | const [isDark, { toggle }] = useDarkreader(defaultDarken === 'dark'); 38 | 39 | useEffect(() => { 40 | setColor(isDark); 41 | }, [isDark]); 42 | 43 | if (!isBrowser()) { 44 | return null; 45 | } 46 | return ( 47 |
58 | { 64 | toggle(); 65 | if (!check) { 66 | localStorage.setItem('procomponents_dark_theme', 'light'); 67 | } else { 68 | localStorage.setItem('procomponents_dark_theme', 'dark'); 69 | } 70 | }} 71 | /> 72 |
73 | ); 74 | }; 75 | 76 | function loadJS(url, callback) { 77 | const script = document.createElement('script'); 78 | script.type = 'text/javascript'; 79 | script.onload = function () { 80 | callback?.(); 81 | }; 82 | script.src = url; 83 | 84 | document.getElementsByTagName('head')[0].appendChild(script); 85 | } 86 | 87 | export default ({ children, ...props }: IRouteComponentProps) => { 88 | const context = useContext(dumiContext); 89 | useEffect(() => { 90 | if (!isBrowser()) { 91 | return null; 92 | } 93 | 94 | loadJS('https://www.googletagmanager.com/gtag/js?id=G-RMBLDHGL1N', function () { 95 | // @ts-ignore 96 | window.dataLayer = window.dataLayer || []; 97 | function gtag() { 98 | // @ts-ignore 99 | dataLayer.push(arguments); 100 | } 101 | // @ts-ignore 102 | gtag('js', new Date()); 103 | // @ts-ignore 104 | gtag('config', 'G-RMBLDHGL1N'); 105 | }); 106 | 107 | (function (h, o, t, j, a, r) { 108 | // @ts-ignore 109 | h.hj = 110 | // @ts-ignore 111 | h.hj || 112 | function () { 113 | // @ts-ignore 114 | (h.hj.q = h.hj.q || []).push(arguments); 115 | }; 116 | // @ts-ignore 117 | h._hjSettings = { hjid: 2036108, hjsv: 6 }; 118 | a = o.getElementsByTagName('head')[0]; 119 | r = o.createElement('script'); 120 | r.async = 1; 121 | // @ts-ignore 122 | r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv; 123 | a.appendChild(r); 124 | })(window, document, 'https://static.hotjar.com/c/hotjar-', '.js?sv='); 125 | }, []); 126 | 127 | const title = useMemo(() => { 128 | if (context.meta.title?.includes('-')) { 129 | return `${context.meta.title}`; 130 | } 131 | if (!context.meta.title) { 132 | return 'ProComponents - 模板组件'; 133 | } 134 | return `${context.meta.title} - ProComponents`; 135 | }, [context]); 136 | 137 | return ( 138 | 139 | 140 | 141 | <> 142 | 143 | {title} 144 | 145 | {children} 146 | {isBrowser() ? : null} 147 | 148 | 149 | 150 | 151 | ); 152 | }; 153 | -------------------------------------------------------------------------------- /theme/useDarkreader.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useMemo } from 'react'; 2 | 3 | export type Action = { 4 | toggle: () => void; 5 | collectCSS: () => Promise; 6 | }; 7 | 8 | export type Result = [boolean, Action]; 9 | 10 | export default function useDarkreader(defaultDarken: boolean = false): [ 11 | boolean, 12 | { 13 | toggle: () => void; 14 | collectCSS: () => Promise; 15 | }, 16 | ] { 17 | const { 18 | enable: enableDarkMode, 19 | disable: disableDarkMode, 20 | exportGeneratedCSS: collectCSS, 21 | setFetchMethod, 22 | } = DarkReader || {}; 23 | 24 | const [isDark, setIsDark] = useState(defaultDarken); 25 | 26 | const defaultTheme = { 27 | brightness: 100, 28 | contrast: 90, 29 | sepia: 10, 30 | }; 31 | 32 | const defaultFixes = { 33 | invert: [], 34 | css: '', 35 | ignoreInlineStyle: ['.react-switch-handle'], 36 | ignoreImageAnalysis: [], 37 | }; 38 | 39 | useEffect(() => { 40 | if (!DarkReader) { 41 | return () => null; 42 | } 43 | setFetchMethod(fetch); 44 | 45 | isDark ? enableDarkMode(defaultTheme, defaultFixes) : disableDarkMode(); 46 | 47 | // unmount 48 | return () => { 49 | disableDarkMode(); 50 | }; 51 | }, [isDark]); 52 | 53 | const action = useMemo(() => { 54 | const toggle = () => setIsDark((prevState) => !prevState); 55 | 56 | return { toggle, collectCSS }; 57 | }, [isDark]); 58 | 59 | return [isDark, action]; 60 | } 61 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "target": "esnext", 5 | "moduleResolution": "node", 6 | "jsx": "preserve", 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noImplicitReturns": true, 12 | "suppressImplicitAnyIndexErrors": true, 13 | "declaration": true, 14 | "skipLibCheck": true, 15 | "paths": { 16 | "@ant-design/pro-skeleton": ["./packages/skeleton/src/index.tsx"], 17 | "@ant-design/pro-list": ["./packages/list/src/index.tsx"], 18 | "@ant-design/pro-descriptions": ["./packages/descriptions/src/index.tsx"], 19 | "@ant-design/pro-utils": ["./packages/utils/src/index.tsx"] 20 | } 21 | }, 22 | "include": [ 23 | "**/src/**/*", 24 | "**/docs/**/*", 25 | "scripts/**/*", 26 | "**/demos", 27 | ".eslintrc.js", 28 | "tests", 29 | "jest.config.js", 30 | "**/fixtures", 31 | "./tests/no-duplicated.ts", 32 | "packages/descriptions/src/demos/.tsx" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.less'; 2 | 3 | interface Window { 4 | DarkReader: any; 5 | } 6 | 7 | declare const DarkReader; 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 3 | const TerserPlugin = require('terser-webpack-plugin'); 4 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 5 | const ProgressBarPlugin = require('progress-bar-webpack-plugin'); 6 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 7 | const { readdirSync } = require('fs'); 8 | 9 | const tailPkgs = readdirSync(path.join(__dirname, 'packages')).filter( 10 | (pkg) => pkg.charAt(0) !== '.', 11 | ); 12 | 13 | // const tailPkgs = ['table']; 14 | 15 | const isCI = process.env.PRO_COMPONENTS_CI === 'CI'; 16 | 17 | const externals = isCI 18 | ? tailPkgs.reduce((pre, value) => { 19 | return { 20 | ...pre, 21 | [`xu-${value}`]: `Pro${value 22 | .toLowerCase() 23 | .replace(/( |^)[a-z]/g, (L) => L.toUpperCase())}`, 24 | }; 25 | }, {}) 26 | : {}; 27 | 28 | console.log(externals); 29 | 30 | const webPackConfigList = []; 31 | 32 | tailPkgs.forEach((pkg) => { 33 | const entry = {}; 34 | entry[`${pkg}`] = `./packages/${pkg}/src/index.tsx`; 35 | if (!isCI) { 36 | entry[`${pkg}.min`] = `./packages/${pkg}/src/index.tsx`; 37 | } 38 | const config = { 39 | entry, 40 | output: { 41 | filename: '[name].js', 42 | library: `Pro${pkg.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())}`, 43 | libraryTarget: 'umd', 44 | path: path.resolve(__dirname, 'packages', pkg, 'dist'), 45 | globalObject: 'this', 46 | }, 47 | mode: 'production', 48 | resolve: { 49 | extensions: ['.ts', '.tsx', '.json', '.css', '.js', '.less'], 50 | }, 51 | optimization: isCI 52 | ? { 53 | minimize: true, 54 | minimizer: [ 55 | new TerserPlugin({ 56 | include: /\.min\.js$/, 57 | }), 58 | new CssMinimizerPlugin({ 59 | include: /\.min\.js$/, 60 | }), 61 | ], 62 | } 63 | : { concatenateModules: false }, 64 | module: { 65 | rules: [ 66 | { 67 | test: /\.(png|jpg|gif|svg)$/i, 68 | type: 'asset', 69 | }, 70 | { 71 | test: /\.jsx?$/, 72 | use: { 73 | loader: 'babel-loader', 74 | options: { 75 | presets: ['@umijs/babel-preset-umi/app'], 76 | plugins: [require('./scripts/replaceLib')], 77 | }, 78 | }, 79 | }, 80 | { 81 | test: /\.tsx?$/, 82 | exclude: /(node_modules|bower_components)/, 83 | use: { 84 | loader: 'babel-loader', 85 | options: { 86 | presets: ['@umijs/babel-preset-umi/app'], 87 | plugins: [require('./scripts/replaceLib')], 88 | }, 89 | }, 90 | }, 91 | { 92 | test: /\.css$/, 93 | use: [ 94 | { 95 | loader: 'style-loader', // creates style nodes from JS strings 96 | }, 97 | { 98 | loader: 'css-loader', // translates CSS into CommonJS 99 | }, 100 | ], 101 | }, 102 | { 103 | test: /\.less$/, 104 | use: [ 105 | { 106 | loader: MiniCssExtractPlugin.loader, 107 | options: { 108 | publicPath: (resourcePath, context) => 109 | `${path.relative(path.dirname(resourcePath), context)}/`, 110 | }, 111 | }, 112 | { 113 | loader: 'css-loader', // translates CSS into CommonJS 114 | }, 115 | { 116 | loader: 'less-loader', 117 | options: { 118 | lessOptions: { 119 | javascriptEnabled: true, 120 | }, 121 | }, 122 | }, 123 | ], 124 | }, 125 | ], 126 | }, 127 | externals: [ 128 | { 129 | react: 'React', 130 | 'react-dom': 'ReactDOM', 131 | antd: 'antd', 132 | moment: 'moment', 133 | ...externals, 134 | }, 135 | ], 136 | plugins: [ 137 | new ProgressBarPlugin(), 138 | // new BundleAnalyzerPlugin(), 139 | new MiniCssExtractPlugin({ 140 | // Options similar to the same options in webpackOptions.output 141 | // both options are optional 142 | filename: '[name].css', 143 | chunkFilename: '[id].css', 144 | }), 145 | ], 146 | }; 147 | webPackConfigList.push(config); 148 | }); 149 | 150 | module.exports = webPackConfigList; 151 | --------------------------------------------------------------------------------