Jelajahi Sumber

Merge branch 'master' into v2

陈帅 7 tahun lalu
induk
melakukan
e3a733098d

+ 23 - 0
.circleci/config.yml

@@ -0,0 +1,23 @@
+version: 2
+jobs:
+  build:
+    docker:
+      - image: circleci/node:8.11.2
+    steps:
+      - checkout
+      - run: npm install
+      - run: npm run build
+  test:
+    docker:
+      - image: circleci/node:8.11.2
+    steps:
+      - checkout
+      - run: sh ./tests/fix_puppeteer.sh
+      - run: npm install
+      - run: npm run test:all
+workflows:
+  version: 2
+  build_and_test:
+    jobs:
+      - build
+      - test

+ 1 - 0
package.json

@@ -48,6 +48,7 @@
     "rollbar": "^2.3.4",
     "rollup": "^0.62.0",
     "rollup-plugin-json": "^3.0.0",
+    "setprototypeof": "^1.1.0",
     "url-polyfill": "^1.0.10"
   },
   "devDependencies": {

+ 16 - 0
src/components/Authorized/CheckPermissions.js

@@ -29,6 +29,14 @@ const checkPermissions = (authority, currentAuthority, target, Exception) => {
     if (authority.indexOf(currentAuthority) >= 0) {
       return target;
     }
+    if (Array.isArray(currentAuthority)) {
+      for (let i = 0; i < currentAuthority.length; i += 1) {
+        const element = currentAuthority[i];
+        if (authority.indexOf(element) >= 0) {
+          return target;
+        }
+      }
+    }
     return Exception;
   }
 
@@ -37,6 +45,14 @@ const checkPermissions = (authority, currentAuthority, target, Exception) => {
     if (authority === currentAuthority) {
       return target;
     }
+    if (Array.isArray(currentAuthority)) {
+      for (let i = 0; i < currentAuthority.length; i += 1) {
+        const element = currentAuthority[i];
+        if (authority.indexOf(element) >= 0) {
+          return target;
+        }
+      }
+    }
     return Exception;
   }
 

+ 18 - 0
src/components/Authorized/CheckPermissions.test.js

@@ -34,4 +34,22 @@ describe('test CheckPermissions', () => {
   it('Correct Function permission authentication', () => {
     expect(checkPermissions(() => true, 'guest', target, error)).toEqual('ok');
   });
+  it('authority is string, currentAuthority is array, return ok', () => {
+    expect(checkPermissions('user', ['user'], target, error)).toEqual('ok');
+  });
+  it('authority is string, currentAuthority is array, return ok', () => {
+    expect(checkPermissions('user', ['user', 'admin'], target, error)).toEqual('ok');
+  });
+  it('authority is array, currentAuthority is array, return ok', () => {
+    expect(checkPermissions(['user', 'admin'], ['user', 'admin'], target, error)).toEqual('ok');
+  });
+  it('Wrong Function permission authentication', () => {
+    expect(checkPermissions(() => false, ['user'], target, error)).toEqual('error');
+  });
+  it('Correct Function permission authentication', () => {
+    expect(checkPermissions(() => true, ['user'], target, error)).toEqual('ok');
+  });
+  it('authority is undefined , return ok', () => {
+    expect(checkPermissions(null, ['user'], target, error)).toEqual('ok');
+  });
 });

+ 2 - 2
src/components/Authorized/Secured.js

@@ -46,8 +46,8 @@ const authorize = (authority, error) => {
   if (!authority) {
     throw new Error('authority is required');
   }
-  return function decideAuthority(targer) {
-    const component = CheckPermissions(authority, targer, classError || Exception403);
+  return function decideAuthority(target) {
+    const component = CheckPermissions(authority, target, classError || Exception403);
     return checkIsInstantiation(component);
   };
 };

+ 4 - 1
src/components/Authorized/renderAuthorize.js

@@ -10,7 +10,10 @@ const renderAuthorize = Authorized => {
       if (currentAuthority.constructor.name === 'Function') {
         CURRENT = currentAuthority();
       }
-      if (currentAuthority.constructor.name === 'String') {
+      if (
+        currentAuthority.constructor.name === 'String' ||
+        currentAuthority.constructor.name === 'Array'
+      ) {
         CURRENT = currentAuthority;
       }
     } else {

+ 8 - 8
src/components/Charts/Bar/index.js

@@ -19,6 +19,14 @@ class Bar extends Component {
     window.removeEventListener('resize', this.resize);
   }
 
+  handleRoot = n => {
+    this.root = n;
+  };
+
+  handleRef = n => {
+    this.node = n;
+  };
+
   @Bind()
   @Debounce(400)
   resize() {
@@ -46,14 +54,6 @@ class Bar extends Component {
     }
   }
 
-  handleRoot = n => {
-    this.root = n;
-  };
-
-  handleRef = n => {
-    this.node = n;
-  };
-
   render() {
     const {
       height,

+ 10 - 1
src/components/Charts/Pie/index.js

@@ -139,8 +139,17 @@ export default class Pie extends Component {
       [styles.legendBlock]: legendBlock,
     });
 
+    const {
+      data: propsData,
+      selected: propsSelected = true,
+      tooltip: propsTooltip = true,
+    } = this.props;
+
+    let data = propsData || [];
+    let selected = propsSelected;
+    let tooltip = propsTooltip;
+
     const defaultColors = colors;
-    let { data, selected, tooltip } = this.props;
     data = data || [];
     selected = selected || true;
     tooltip = tooltip || true;

+ 2 - 1
src/components/Charts/Radar/index.js

@@ -16,7 +16,8 @@ export default class Radar extends Component {
   }
 
   componentDidUpdate(preProps) {
-    if (this.props.data !== preProps.data) {
+    const { data } = this.props;
+    if (data !== preProps.data) {
       this.getLegendData();
     }
   }

+ 2 - 1
src/components/Charts/TagCloud/index.js

@@ -27,7 +27,8 @@ class TagCloud extends Component {
   }
 
   componentDidUpdate(preProps) {
-    if (JSON.stringify(preProps.data) !== JSON.stringify(this.props.data)) {
+    const { data } = this.props;
+    if (JSON.stringify(preProps.data) !== JSON.stringify(data)) {
       this.renderChart(this.props);
     }
   }

+ 1 - 1
src/components/HeaderSearch/index.js

@@ -87,8 +87,8 @@ export default class HeaderSearch extends PureComponent {
 
   render() {
     const { className, placeholder, ...restProps } = this.props;
-    delete restProps.defaultOpen; // for rc-select not affected
     const { searchMode, value } = this.state;
+    delete restProps.defaultOpen; // for rc-select not affected
     const inputClass = classNames(styles.input, {
       [styles.show]: searchMode,
     });

+ 1 - 1
src/components/Login/index.js

@@ -74,8 +74,8 @@ class Login extends Component {
   handleSubmit = e => {
     e.preventDefault();
     const { active, type } = this.state;
-    const activeFileds = active[type];
     const { form, onSubmit } = this.props;
+    const activeFileds = active[type];
     form.validateFields(activeFileds, { force: true }, (err, values) => {
       onSubmit(err, values);
     });

+ 1 - 1
src/components/PageHeader/index.js

@@ -180,6 +180,7 @@ export default class PageHeader extends PureComponent {
       tabBarExtraContent,
       loading = false,
     } = this.props;
+    const { breadcrumb } = this.state;
 
     const clsString = classNames(styles.pageHeader, className);
     const activeKeyProps = {};
@@ -189,7 +190,6 @@ export default class PageHeader extends PureComponent {
     if (tabActiveKey !== undefined) {
       activeKeyProps.activeKey = tabActiveKey;
     }
-    const { breadcrumb } = this.state;
     return (
       <Card className={clsString} bodyStyle={{ padding: 0 }} loading={loading}>
         {breadcrumb}

+ 4 - 3
src/components/TopNavHeader/index.js

@@ -6,13 +6,14 @@ import styles from './index.less';
 
 export default class TopNavHeader extends PureComponent {
   render() {
+    const { theme, grid, logo } = this.props;
     return (
-      <div className={`${styles.head} ${this.props.theme === 'light' ? styles.light : ''}`}>
-        <div className={`${styles.main} ${this.props.grid === 'Wide' ? styles.wide : ''}`}>
+      <div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
+        <div className={`${styles.main} ${grid === 'Wide' ? styles.wide : ''}`}>
           <div className={styles.left}>
             <div className={styles.logo} key="logo" id="logo">
               <Link to="/">
-                <img src={this.props.logo} alt="logo" />
+                <img src={logo} alt="logo" />
                 <h1>Ant Design Pro</h1>
               </Link>
             </div>

+ 6 - 0
src/e2e/login.e2e.js

@@ -17,6 +17,9 @@ describe('Login', () => {
   afterEach(() => page.close());
 
   it('should login with failure', async () => {
+    await page.waitForSelector('#userName', {
+      timeout: 2000,
+    });
     await page.type('#userName', 'mockuser');
     await page.type('#password', 'wrong_password');
     await page.click('button[type="submit"]');
@@ -24,6 +27,9 @@ describe('Login', () => {
   });
 
   it('should login successfully', async () => {
+    await page.waitForSelector('#userName', {
+      timeout: 2000,
+    });
     await page.type('#userName', 'admin');
     await page.type('#password', '888888');
     await page.click('button[type="submit"]');

+ 1 - 0
src/index.ejs

@@ -7,6 +7,7 @@
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <title>Ant Design Pro</title>
   <link rel="icon" href="/favicon.png" type="image/x-icon">
+  <script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
   <script src="https://gw.alipayobjects.com/os/rmsportal/nGVBgVyXzzmbAqevIAPy.js">
   </script>
   <script src=" https://gw.alipayobjects.com/os/rmsportal/TKSqiyoUxzrHoMwjViwA.js "></script>

+ 1 - 2
src/index.js

@@ -1,5 +1,4 @@
-import '@babel/polyfill';
-import 'url-polyfill';
+import './polyfill';
 import dva from 'dva';
 
 import createHistory from 'history/createHashHistory';

+ 4 - 0
src/layouts/BasicLayout.js

@@ -56,6 +56,10 @@ const query = {
   },
   'screen-xl': {
     minWidth: 1200,
+    maxWidth: 1599,
+  },
+  'screen-xxl': {
+    minWidth: 1600,
   },
 };
 

+ 3 - 2
src/layouts/GridContent.js

@@ -4,11 +4,12 @@ import styles from './GridContent.less';
 
 class GridContent extends PureComponent {
   render() {
+    const { grid, children } = this.props;
     let className = `${styles.main}`;
-    if (this.props.grid === 'Wide') {
+    if (grid === 'Wide') {
       className = `${styles.main} ${styles.wide}`;
     }
-    return <div className={className}>{this.props.children}</div>;
+    return <div className={className}>{children}</div>;
   }
 }
 

+ 10 - 2
src/layouts/UserLayout.js

@@ -5,7 +5,7 @@ import { Icon } from 'antd';
 import GlobalFooter from '../components/GlobalFooter';
 import styles from './UserLayout.less';
 import logo from '../assets/logo.svg';
-import { getRoutes } from '../utils/utils';
+import { getRoutes, getPageQuery, getQueryPath } from '../utils/utils';
 
 const links = [
   {
@@ -31,6 +31,14 @@ const copyright = (
   </Fragment>
 );
 
+function getLoginPathWithRedirectPath() {
+  const params = getPageQuery();
+  const { redirect } = params;
+  return getQueryPath('/user/login', {
+    redirect,
+  });
+}
+
 class UserLayout extends React.PureComponent {
   getPageTitle() {
     const { routerData, location } = this.props;
@@ -66,7 +74,7 @@ class UserLayout extends React.PureComponent {
                   exact={item.exact}
                 />
               ))}
-              <Redirect exact from="/user" to="/user/login" />
+              <Redirect from="/user" to={getLoginPathWithRedirectPath()} />
             </Switch>
           </div>
           <GlobalFooter links={links} copyright={copyright} />

+ 35 - 20
src/models/login.js

@@ -1,6 +1,8 @@
 import { routerRedux } from 'dva/router';
+import { stringify } from 'qs';
 import { fakeAccountLogin, getFakeCaptcha } from '../services/api';
 import { setAuthority } from '../utils/authority';
+import { getPageQuery } from '../utils/utils';
 import { reloadAuthorized } from '../utils/Authorized';
 
 export default {
@@ -20,32 +22,45 @@ export default {
       // Login successfully
       if (response.status === 'ok') {
         reloadAuthorized();
-        yield put(routerRedux.push('/'));
-      }
-    },
-    *logout(_, { put, select }) {
-      try {
-        // get location pathname
         const urlParams = new URL(window.location.href);
-        const pathname = yield select(state => state.routing.location.pathname);
-        // add the parameters in the url
-        urlParams.searchParams.set('redirect', pathname);
-        window.history.replaceState(null, 'login', urlParams.href);
-      } finally {
-        yield put({
-          type: 'changeLoginStatus',
-          payload: {
-            status: false,
-            currentAuthority: 'guest',
-          },
-        });
-        reloadAuthorized();
-        yield put(routerRedux.push('/user/login'));
+        const params = getPageQuery();
+        let { redirect } = params;
+        if (redirect) {
+          const redirectUrlParams = new URL(redirect);
+          if (redirectUrlParams.origin === urlParams.origin) {
+            redirect = redirect.substr(urlParams.origin.length);
+            if (redirect.startsWith('/#')) {
+              redirect = redirect.substr(2);
+            }
+          } else {
+            window.location.href = redirect;
+            return;
+          }
+        }
+        yield put(routerRedux.replace(redirect || '/'));
       }
     },
     *getCaptcha({ payload }, { call }) {
       yield call(getFakeCaptcha, payload);
     },
+    *logout(_, { put }) {
+      yield put({
+        type: 'changeLoginStatus',
+        payload: {
+          status: false,
+          currentAuthority: 'guest',
+        },
+      });
+      reloadAuthorized();
+      yield put(
+        routerRedux.push({
+          pathname: '/user/login',
+          search: stringify({
+            redirect: window.location.href,
+          }),
+        })
+      );
+    },
   },
 
   reducers: {

+ 12 - 0
src/polyfill.js

@@ -0,0 +1,12 @@
+import '@babel/polyfill';
+import 'url-polyfill';
+import setprototypeof from 'setprototypeof';
+
+// React depends on set/map/requestAnimationFrame
+// https://reactjs.org/docs/javascript-environment-requirements.html
+// import 'core-js/es6/set';
+// import 'core-js/es6/map';
+// import 'raf/polyfill'; 只兼容到IE10不需要,况且fetch的polyfill whatwg-fetch也只兼容到IE10
+
+// https://github.com/umijs/umi/issues/413
+Object.setPrototypeOf = setprototypeof;

+ 4 - 1
src/router.js

@@ -5,6 +5,7 @@ import zhCN from 'antd/lib/locale-provider/zh_CN';
 import dynamic from 'dva/dynamic';
 import { getRouterData } from './common/router';
 import Authorized from './utils/Authorized';
+import { getQueryPath } from './utils/utils';
 import styles from './index.less';
 
 const { ConnectedRouter } = routerRedux;
@@ -26,7 +27,9 @@ function RouterConfig({ history, app }) {
             path="/"
             render={props => <BasicLayout {...props} />}
             authority={['admin', 'user']}
-            redirectPath="/user/login"
+            redirectPath={getQueryPath('/user/login', {
+              redirect: window.location.href,
+            })}
           />
         </Switch>
       </ConnectedRouter>

+ 6 - 4
src/routes/Forms/AdvancedForm.js

@@ -73,10 +73,12 @@ class AdvancedForm extends PureComponent {
   resizeFooterToolbar = () => {
     requestAnimationFrame(() => {
       const sider = document.querySelectorAll('.ant-layout-sider')[0];
-      const width = `calc(100% - ${sider.style.width})`;
-      const { width: stateWidth } = this.state;
-      if (stateWidth !== width) {
-        this.setState({ width });
+      if (sider) {
+        const width = `calc(100% - ${sider.style.width})`;
+        const { width: stateWidth } = this.state;
+        if (stateWidth !== width) {
+          this.setState({ width });
+        }
       }
     });
   };

+ 1 - 0
src/utils/authority.js

@@ -1,5 +1,6 @@
 // use localStorage to store the authority info, which might be sent from server in actual project.
 export function getAuthority() {
+  // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
   return localStorage.getItem('antd-pro-authority') || 'admin';
 }
 

+ 13 - 0
src/utils/utils.js

@@ -1,5 +1,6 @@
 import moment from 'moment';
 import React from 'react';
+import { parse, stringify } from 'qs';
 
 export function fixedZero(val) {
   return val * 1 < 10 ? `0${val}` : val;
@@ -162,6 +163,18 @@ export function getRoutes(path, routerData) {
   return renderRoutes;
 }
 
+export function getPageQuery() {
+  return parse(window.location.href.split('?')[1]);
+}
+
+export function getQueryPath(path = '', query = {}) {
+  const search = stringify(query);
+  if (search.length) {
+    return `${path}?${search}`;
+  }
+  return path;
+}
+
 /* eslint no-useless-escape:0 */
 const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
 

+ 8 - 0
tests/fix_puppeteer.sh

@@ -0,0 +1,8 @@
+#!/bin/bash
+
+sudo apt-get update
+sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
+  libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
+  libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
+  libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
+  ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget