Browse Source

Merge branch 'master' into v4

陈帅 6 years atrás
parent
commit
c82c45324d

+ 4 - 0
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

+ 7 - 1
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',
+    ],
   },
 };

+ 7 - 2
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",

+ 16 - 0
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 <div className={className}>{children}</div>;
+};
+
+export default connect(({ setting }) => ({
+  contentWidth: setting.contentWidth,
+}))(GridContent);

+ 10 - 0
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;
+  }
+}

+ 116 - 0
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 ? (
+    <span>{route.breadcrumbName}</span>
+  ) : (
+    <Link to={paths.join('/')}>{route.breadcrumbName}</Link>
+  );
+};
+
+const renderItemLocal = item => {
+  if (item.locale) {
+    return <FormattedMessage id={item.locale} defaultMessage={item.name} />;
+  }
+  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 {};
+};

+ 104 - 0
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 ? (
+    <Tabs
+      className={styles.tabs}
+      {...activeKeyProps}
+      onChange={key => {
+        if (onTabChange) {
+          onTabChange(key);
+        }
+      }}
+      tabBarExtraContent={tabBarExtraContent}
+    >
+      {tabList.map(item => (
+        <Tabs.TabPane tab={item.tab} key={item.key} />
+      ))}
+    </Tabs>
+  ) : null;
+};
+
+const PageHeaderWrapper = ({
+  children,
+  contentWidth,
+  wrapperClassName,
+  top,
+  title,
+  content,
+  logo,
+  extraContent,
+  ...restProps
+}) => {
+  return (
+    <div style={{ margin: '-24px -24px 0' }} className={classNames(classNames, styles.main)}>
+      {top}
+      {title && content && (
+        <MenuContext.Consumer>
+          {value => {
+            return (
+              <PageHeader
+                wide={contentWidth === 'Fixed'}
+                title={
+                  <Title
+                    level={4}
+                    style={{
+                      marginBottom: 0,
+                    }}
+                  >
+                    {title}
+                  </Title>
+                }
+                key="pageheader"
+                {...restProps}
+                breadcrumb={conversionBreadcrumbList({
+                  ...value,
+                  ...restProps,
+                  home: <FormattedMessage id="menu.home" defaultMessage="Home" />,
+                })}
+                className={styles.pageHeader}
+                linkElement={Link}
+                footer={renderFooter(restProps)}
+              >
+                <div className={styles.detail}>
+                  {logo && <div className={styles.logo}>{logo}</div>}
+                  <div className={styles.main}>
+                    <div className={styles.row}>
+                      {content && <div className={styles.content}>{content}</div>}
+                      {extraContent && <div className={styles.extraContent}>{extraContent}</div>}
+                    </div>
+                  </div>
+                </div>
+              </PageHeader>
+            );
+          }}
+        </MenuContext.Consumer>
+      )}
+      {children ? (
+        <div className={styles['children-content']}>
+          <GridContent>{children}</GridContent>
+        </div>
+      ) : null}
+    </div>
+  );
+};
+
+export default connect(({ setting }) => ({
+  contentWidth: setting.contentWidth,
+}))(PageHeaderWrapper);

+ 110 - 0
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;
+  }
+}

+ 37 - 0
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));
+  });
+});

+ 19 - 0
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();
+  });
+});

+ 4 - 1
src/layouts/Header.tsx

@@ -136,7 +136,10 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
     const isTop = layout === 'topmenu';
     const width = this.getHeadWidth();
     const HeaderDom = visible ? (
-      <Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}>
+      <Header
+        style={{ padding: 0, width, zIndex: 2 }}
+        className={fixedHeader ? styles.fixedHeader : ''}
+      >
         {isTop && !isMobile ? (
           <TopNavHeader
             theme={navTheme}

+ 5 - 0
src/utils/authority.ts

@@ -1,4 +1,6 @@
 // use localStorage to store the authority info, which might be sent from server in actual project.
+const { NODE_ENV } = process.env;
+
 export function getAuthority(str?: string): any {
   // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
   const authorityString =
@@ -13,6 +15,9 @@ export function getAuthority(str?: string): any {
   if (typeof authority === 'string') {
     return [authority];
   }
+  if (!authority && NODE_ENV !== 'production') {
+    return ['admin'];
+  }
   return authority;
 }