diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9c4c97e1ad91a23e84b499ffdae0eac65a9cb579..2df685a5dd6c44538cf54537cd2ff99ffa7b99ba 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -52,6 +52,10 @@ jobs: displayName: install - script: npm run lint displayName: lint + - script: npm run test:all + env: + PROGRESS: none + displayName: test - script: npm run build env: PROGRESS: none diff --git a/jest-puppeteer.config.js b/jest-puppeteer.config.js index cded345494729be647ba25d868f38e33ad5eaa01..21b41e4aaf354b1081863e45321f229f7ffe9416 100644 --- a/jest-puppeteer.config.js +++ b/jest-puppeteer.config.js @@ -1,6 +1,12 @@ // ps https://github.com/GoogleChrome/puppeteer/issues/3120 module.exports = { launch: { - args: ['--disable-gpu', '--disable-dev-shm-usage', '--no-first-run', '--no-zygote'], + args: [ + '--disable-gpu', + '--disable-dev-shm-usage', + '--no-first-run', + '--no-zygote', + '--no-sandbox', + ], }, }; diff --git a/package.json b/package.json index d69874b5649fb2514261d3dac601084c7b7b226d..21e33fec1fe968d411dc3ee879fc5ba4d54d89ae 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "An out-of-box UI solution for enterprise applications", "scripts": { "analyze": "cross-env ANALYZE=1 umi build", - "build": "umi build", + "build": "umi build && npm run functions:build", "dev": "cross-env APP_TYPE=site umi dev", "dev:no-mock": "cross-env MOCK=none umi dev", "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", @@ -73,9 +73,13 @@ "react-container-query": "^0.11.0", "react-copy-to-clipboard": "^5.0.1", "react-document-title": "^2.0.3", + "react-dom": "^16.7.0", + "react-fittext": "^1.0.0", + "react-media": "^1.9.2", "react-media-hook2": "^1.0.2", "umi": "^2.6.10", - "umi-request": "^1.0.0" + "umi-plugin-react": "^1.7.2", + "umi-request": "^1.0.5" }, "devDependencies": { "@types/classnames": "^2.2.7", @@ -110,6 +114,7 @@ "mockjs": "^1.0.1-beta3", "netlify-lambda": "^1.4.3", "prettier": "^1.16.4", + "serverless-http": "^1.9.1", "slash2": "^2.0.0", "stylelint": "^9.10.1", "stylelint-config-css-modules": "^1.3.0", diff --git a/src/components/PageHeaderWrapper/GridContent.js b/src/components/PageHeaderWrapper/GridContent.js new file mode 100644 index 0000000000000000000000000000000000000000..fee6a9318c263249ffd3aaf67fc656e471aa8984 --- /dev/null +++ b/src/components/PageHeaderWrapper/GridContent.js @@ -0,0 +1,16 @@ +import React from 'react'; +import { connect } from 'dva'; +import styles from './GridContent.less'; + +const GridContent = props => { + const { contentWidth, children } = props; + let className = `${styles.main}`; + if (contentWidth === 'Fixed') { + className = `${styles.main} ${styles.wide}`; + } + return
{children}
; +}; + +export default connect(({ setting }) => ({ + contentWidth: setting.contentWidth, +}))(GridContent); diff --git a/src/components/PageHeaderWrapper/GridContent.less b/src/components/PageHeaderWrapper/GridContent.less new file mode 100644 index 0000000000000000000000000000000000000000..d5496e9ecb95318c38a30f1369d35e8fcf583758 --- /dev/null +++ b/src/components/PageHeaderWrapper/GridContent.less @@ -0,0 +1,10 @@ +.main { + width: 100%; + height: 100%; + min-height: 100%; + transition: 0.3s; + &.wide { + max-width: 1200px; + margin: 0 auto; + } +} diff --git a/src/components/PageHeaderWrapper/breadcrumb.js b/src/components/PageHeaderWrapper/breadcrumb.js new file mode 100644 index 0000000000000000000000000000000000000000..02fe66fd26530f68bd99e6a3c863f384a82e611d --- /dev/null +++ b/src/components/PageHeaderWrapper/breadcrumb.js @@ -0,0 +1,116 @@ +import React from 'react'; +import pathToRegexp from 'path-to-regexp'; +import Link from 'umi/link'; +import { FormattedMessage } from 'umi-plugin-react/locale'; +import { urlToList } from '../_utils/pathTools'; + +// 渲染Breadcrumb 子节点 +// Render the Breadcrumb child node +const itemRender = (route, params, routes, paths) => { + const last = routes.indexOf(route) === routes.length - 1; + return last || !route.component ? ( + {route.breadcrumbName} + ) : ( + {route.breadcrumbName} + ); +}; + +const renderItemLocal = item => { + if (item.locale) { + return ; + } + return item.name; +}; + +export const getBreadcrumb = (breadcrumbNameMap, url) => { + let breadcrumb = breadcrumbNameMap[url]; + if (!breadcrumb) { + Object.keys(breadcrumbNameMap).forEach(item => { + if (pathToRegexp(item).test(url)) { + breadcrumb = breadcrumbNameMap[item]; + } + }); + } + return breadcrumb || {}; +}; + +export const getBreadcrumbProps = props => { + const { routes, params, location, breadcrumbNameMap } = props; + return { + routes, + params, + routerLocation: location, + breadcrumbNameMap, + }; +}; + +// Generated according to props +const conversionFromProps = props => { + const { breadcrumbList } = props; + return breadcrumbList.map(item => { + const { title, href } = item; + return { + path: href, + breadcrumbName: title, + }; + }); +}; + +const conversionFromLocation = (routerLocation, breadcrumbNameMap, props) => { + const { home } = props; + // Convert the url to an array + const pathSnippets = urlToList(routerLocation.pathname); + // Loop data mosaic routing + const extraBreadcrumbItems = pathSnippets.map(url => { + const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url); + if (currentBreadcrumb.inherited) { + return null; + } + const name = renderItemLocal(currentBreadcrumb); + const { hideInBreadcrumb } = currentBreadcrumb; + return name && !hideInBreadcrumb + ? { + path: url, + breadcrumbName: name, + } + : null; + }); + // Add home breadcrumbs to your head if defined + if (home) { + extraBreadcrumbItems.unshift({ + path: '/', + breadcrumbName: home, + }); + } + return extraBreadcrumbItems; +}; + +/** + * 将参数转化为面包屑 + * Convert parameters into breadcrumbs + */ +export const conversionBreadcrumbList = props => { + const { breadcrumbList } = props; + const { routes, params, routerLocation, breadcrumbNameMap } = getBreadcrumbProps(props); + if (breadcrumbList && breadcrumbList.length) { + return conversionFromProps(); + } + // 如果传入 routes 和 params 属性 + // If pass routes and params attributes + if (routes && params) { + return { + routes: routes.filter(route => route.breadcrumbName), + params, + itemRender, + }; + } + // 根据 location 生成 面包屑 + // Generate breadcrumbs based on location + if (routerLocation && routerLocation.pathname) { + return { + routes: conversionFromLocation(routerLocation, breadcrumbNameMap, props), + itemRender, + }; + } + return {}; +}; diff --git a/src/components/PageHeaderWrapper/index.js b/src/components/PageHeaderWrapper/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7a766834c4ac39cd3628409d45fe3cd09bff1fe7 --- /dev/null +++ b/src/components/PageHeaderWrapper/index.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { FormattedMessage } from 'umi-plugin-react/locale'; +import Link from 'umi/link'; +import { PageHeader, Tabs, Typography } from 'antd'; +import { connect } from 'dva'; +import classNames from 'classnames'; +import GridContent from './GridContent'; +import styles from './index.less'; +import MenuContext from '@/layouts/MenuContext'; +import { conversionBreadcrumbList } from './breadcrumb'; + +const { Title } = Typography; + +/** + * render Footer tabList + * In order to be compatible with the old version of the PageHeader + * basically all the functions are implemented. + */ +const renderFooter = ({ tabList, activeKeyProps, onTabChange, tabBarExtraContent }) => { + return tabList && tabList.length ? ( + { + if (onTabChange) { + onTabChange(key); + } + }} + tabBarExtraContent={tabBarExtraContent} + > + {tabList.map(item => ( + + ))} + + ) : null; +}; + +const PageHeaderWrapper = ({ + children, + contentWidth, + wrapperClassName, + top, + title, + content, + logo, + extraContent, + ...restProps +}) => { + return ( +
+ {top} + {title && content && ( + + {value => { + return ( + + {title} + + } + key="pageheader" + {...restProps} + breadcrumb={conversionBreadcrumbList({ + ...value, + ...restProps, + home: , + })} + className={styles.pageHeader} + linkElement={Link} + footer={renderFooter(restProps)} + > +
+ {logo &&
{logo}
} +
+
+ {content &&
{content}
} + {extraContent &&
{extraContent}
} +
+
+
+
+ ); + }} +
+ )} + {children ? ( +
+ {children} +
+ ) : null} +
+ ); +}; + +export default connect(({ setting }) => ({ + contentWidth: setting.contentWidth, +}))(PageHeaderWrapper); diff --git a/src/components/PageHeaderWrapper/index.less b/src/components/PageHeaderWrapper/index.less new file mode 100644 index 0000000000000000000000000000000000000000..119585bbaef2af9a3a3f4dd90ce882a50a5a98e8 --- /dev/null +++ b/src/components/PageHeaderWrapper/index.less @@ -0,0 +1,110 @@ +@import '~antd/lib/style/themes/default.less'; + +.children-content { + margin: 24px 24px 0; +} + +.main { + :global { + .ant-page-header { + padding: 16px 32px 0; + background: #fff; + border-bottom: 1px solid #e8e8e8; + } + } + + .wide { + max-width: 1200px; + margin: auto; + } + .detail { + display: flex; + } + + .row { + display: flex; + width: 100%; + } + + .logo { + flex: 0 1 auto; + margin-right: 16px; + padding-top: 1px; + > img { + display: block; + width: 28px; + height: 28px; + border-radius: @border-radius-base; + } + } + + .title-content { + margin-bottom: 16px; + } + + @media screen and (max-width: @screen-sm) { + .content { + margin: 24px 0 0; + } + } + + .title, + .content { + flex: auto; + } + + .extraContent, + .main { + flex: 0 1 auto; + } + + .main { + width: 100%; + } + + .title { + margin-bottom: 16px; + } + + .logo, + .content, + .extraContent { + margin-bottom: 16px; + } + + .extraContent { + min-width: 242px; + margin-left: 88px; + text-align: right; + } +} + +@media screen and (max-width: @screen-xl) { + .extraContent { + margin-left: 44px; + } +} + +@media screen and (max-width: @screen-lg) { + .extraContent { + margin-left: 20px; + } +} + +@media screen and (max-width: @screen-md) { + .row { + display: block; + } + + .action, + .extraContent { + margin-left: 0; + text-align: left; + } +} + +@media screen and (max-width: @screen-sm) { + .detail { + display: block; + } +} diff --git a/src/e2e/baseLayout.e2e.js b/src/e2e/baseLayout.e2e.js new file mode 100644 index 0000000000000000000000000000000000000000..8e534df6a56641f0ba4049d0400bdc1d3e10ed35 --- /dev/null +++ b/src/e2e/baseLayout.e2e.js @@ -0,0 +1,37 @@ +const RouterConfig = []; +const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; + +function formatter(data) { + return data + .reduce((pre, item) => { + if (item.routes) { + pre.push(item.routes[0].path); + } else { + pre.push(item.path); + } + return pre; + }, []) + .filter(item => item); +} + +describe('Homepage', () => { + const testPage = path => async () => { + await page.goto(`${BASE_URL}${path}`); + await page.waitForSelector('footer', { + timeout: 2000, + }); + const haveFooter = await page.evaluate( + () => document.getElementsByTagName('footer').length > 0, + ); + expect(haveFooter).toBeTruthy(); + }; + + beforeAll(async () => { + jest.setTimeout(1000000); + await page.setCacheEnabled(false); + }); + const routers = formatter(RouterConfig[1].routes); + routers.forEach(route => { + it(`test pages ${route}`, testPage(route)); + }); +}); diff --git a/src/e2e/topMenu.e2e.js b/src/e2e/topMenu.e2e.js new file mode 100644 index 0000000000000000000000000000000000000000..09f2987fe7d482b760d54321be4ec67774c8141e --- /dev/null +++ b/src/e2e/topMenu.e2e.js @@ -0,0 +1,19 @@ +const BASE_URL = `http://localhost:${process.env.PORT || 8000}`; + +describe('Homepage', () => { + beforeAll(async () => { + jest.setTimeout(1000000); + }); + + it('topmenu should have footer', async () => { + const params = '/form/basic-form?navTheme=light&layout=topmenu'; + await page.goto(`${BASE_URL}${params}`); + await page.waitForSelector('footer', { + timeout: 2000, + }); + const haveFooter = await page.evaluate( + () => document.getElementsByTagName('footer').length > 0, + ); + expect(haveFooter).toBeTruthy(); + }); +}); diff --git a/src/layouts/Header.tsx b/src/layouts/Header.tsx index c00145114f495d48102f66325ac6c6cb99b8a0bd..12a160ee9e38c1f78af90744adb8248bf237b945 100644 --- a/src/layouts/Header.tsx +++ b/src/layouts/Header.tsx @@ -136,7 +136,10 @@ class HeaderView extends Component { const isTop = layout === 'topmenu'; const width = this.getHeadWidth(); const HeaderDom = visible ? ( -
+
{isTop && !isMobile ? (