Browse Source

[v4] transform typescript (#3702)

* merge v3 to v4

* src/components/IconFont

* src/components/PageLoading

* src/components/SelectLang

* src/components/SettingDrawer

* remove e2e and test

* src/components/TopNavHeader

* src/components/GlobalHeader

* src/components/HeaderDropdown

* src/components/HeaderSearch

* src/components/TopNavHeader

* fix error

* mock

* move defaultSettings

* global.txs

* src/locales

* remove lint mock

* fix ci test error

* change PureComponent to Component, interface IDefaultSettings

* Don't prefix interface with I
Close: #3706

* strictNullChecks set true
陈小聪 7 years ago
parent
commit
1abac6a331
89 changed files with 772 additions and 643 deletions
  1. 5 4
      config/config.js
  2. 20 2
      src/defaultSettings.js
  3. 0 0
      config/plugin.config.ts
  4. 0 36
      e2e/baseLayout.e2e.js
  5. 0 15
      e2e/home.e2e.js
  6. 0 18
      e2e/topMenu.e2e.js
  7. 0 34
      e2e/userLayout.e2e.js
  8. 0 0
      mock/notices.ts
  9. 0 0
      mock/route.ts
  10. 0 0
      mock/user.ts
  11. 8 5
      package.json
  12. 1 1
      src/app.js
  13. 32 8
      src/components/GlobalHeader/RightContent.js
  14. 15 7
      src/components/GlobalHeader/index.js
  15. 0 2
      src/components/HeaderDropdown/index.d.ts
  16. 0 13
      src/components/HeaderDropdown/index.js
  17. 22 0
      src/components/HeaderDropdown/index.tsx
  18. 0 15
      src/components/HeaderSearch/index.d.ts
  19. 22 14
      src/components/HeaderSearch/index.js
  20. 3 1
      src/components/IconFont/index.js
  21. 2 1
      src/components/PageLoading/index.js
  22. 0 49
      src/components/SelectLang/index.js
  23. 51 0
      src/components/SelectLang/index.tsx
  24. 6 1
      src/components/SettingDrawer/BlockCheckbox.js
  25. 16 3
      src/components/SettingDrawer/ThemeColor.js
  26. 16 5
      src/components/SettingDrawer/index.js
  27. 38 8
      src/components/SiderMenu/BaseMenu.js
  28. 0 39
      src/components/SiderMenu/SiderMenu.test.js
  29. 37 17
      src/components/SiderMenu/SiderMenu.js
  30. 1 0
      src/components/SiderMenu/SiderMenuUtils.js
  31. 14 3
      src/components/SiderMenu/index.js
  32. 39 3
      src/components/TopNavHeader/index.js
  33. 0 17
      src/components/_utils/pathTools.test.js
  34. 0 0
      src/components/_utils/pathTools.ts
  35. 4 4
      src/global.js
  36. 0 173
      src/layouts/BasicLayout.js
  37. 153 0
      src/layouts/BasicLayout.tsx
  38. 0 3
      src/layouts/BlankLayout.js
  39. 9 0
      src/layouts/BlankLayout.tsx
  40. 1 1
      src/layouts/Footer.js
  41. 31 11
      src/layouts/Header.js
  42. 1 1
      src/layouts/MenuContext.js
  43. 17 9
      src/layouts/UserLayout.js
  44. 0 0
      src/locales/en-US.ts
  45. 0 0
      src/locales/en-US/component.ts
  46. 0 0
      src/locales/en-US/globalHeader.ts
  47. 0 0
      src/locales/en-US/menu.ts
  48. 0 0
      src/locales/en-US/pwa.ts
  49. 0 0
      src/locales/en-US/settingDrawer.ts
  50. 0 0
      src/locales/en-US/settings.ts
  51. 0 0
      src/locales/pt-BR.ts
  52. 0 0
      src/locales/pt-BR/component.ts
  53. 0 0
      src/locales/pt-BR/globalHeader.ts
  54. 0 0
      src/locales/pt-BR/menu.ts
  55. 0 0
      src/locales/pt-BR/pwa.ts
  56. 0 0
      src/locales/pt-BR/settingDrawer.ts
  57. 0 0
      src/locales/pt-BR/settings.ts
  58. 0 0
      src/locales/zh-CN.ts
  59. 0 0
      src/locales/zh-CN/component.ts
  60. 0 0
      src/locales/zh-CN/globalHeader.ts
  61. 0 0
      src/locales/zh-CN/menu.ts
  62. 0 0
      src/locales/zh-CN/pwa.ts
  63. 0 0
      src/locales/zh-CN/settingDrawer.ts
  64. 0 0
      src/locales/zh-CN/settings.ts
  65. 0 0
      src/locales/zh-TW.ts
  66. 0 0
      src/locales/zh-TW/component.ts
  67. 0 0
      src/locales/zh-TW/globalHeader.ts
  68. 0 0
      src/locales/zh-TW/menu.ts
  69. 0 0
      src/locales/zh-TW/pwa.ts
  70. 0 0
      src/locales/zh-TW/settingDrawer.ts
  71. 0 0
      src/locales/zh-TW/settings.ts
  72. 28 3
      src/models/global.js
  73. 42 13
      src/models/menu.js
  74. 20 8
      src/models/setting.js
  75. 33 1
      src/models/user.js
  76. 0 44
      src/pages/Authorized.js
  77. 45 0
      src/pages/Authorized.tsx
  78. 0 0
      src/pages/Welcome.tsx
  79. 3 3
      src/services/user.js
  80. 12 0
      src/typings.d.ts
  81. 0 0
      src/utils/Authorized.ts
  82. 1 0
      src/utils/authority.test.js
  83. 2 2
      src/utils/authority.js
  84. 16 5
      src/utils/getPageTitle.js
  85. 0 0
      src/utils/request.ts
  86. 0 38
      src/utils/utils.test.js
  87. 0 0
      src/utils/utils.ts
  88. 3 2
      tsconfig.json
  89. 3 1
      tslint.json

+ 5 - 4
config/config.js

@@ -1,13 +1,14 @@
 // https://umijs.org/config/
 import os from 'os';
-import webpackPlugin from './plugin.config';
-import defaultSettings from '../src/defaultSettings';
 import slash from 'slash2';
+import { IPlugin } from 'umi-types';
+import defaultSettings from './defaultSettings';
+import webpackPlugin from './plugin.config';
 
 const { pwa, primaryColor } = defaultSettings;
-const { NODE_ENV, APP_TYPE, TEST } = process.env;
+const { APP_TYPE, TEST } = process.env;
 
-const plugins = [
+const plugins: IPlugin[] = [
   [
     'umi-plugin-react',
     {

+ 20 - 2
src/defaultSettings.js

@@ -1,4 +1,21 @@
-module.exports = {
+export declare type SiderTheme = 'light' | 'dark';
+
+export interface DefaultSettings {
+  navTheme: string | SiderTheme;
+  primaryColor: string;
+  layout: string;
+  contentWidth: string;
+  fixedHeader: boolean;
+  autoHideHeader: boolean;
+  fixSiderbar: boolean;
+  menu: { disableLocal: boolean };
+  title: string;
+  pwa: boolean;
+  iconfontUrl: string;
+  colorWeak: boolean;
+}
+
+export default {
   navTheme: 'dark', // theme for nav menu
   primaryColor: '#1890FF', // primary color of ant design
   layout: 'sidemenu', // nav menu position: sidemenu or topmenu
@@ -6,6 +23,7 @@ module.exports = {
   fixedHeader: false, // sticky header
   autoHideHeader: false, // auto hide header
   fixSiderbar: false, // sticky siderbar
+  colorWeak: false,
   menu: {
     disableLocal: false,
   },
@@ -15,4 +33,4 @@ module.exports = {
   // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
   // 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
   iconfontUrl: '',
-};
+} as DefaultSettings;

config/plugin.config.js → config/plugin.config.ts


+ 0 - 36
e2e/baseLayout.e2e.js

@@ -1,36 +0,0 @@
-import config from '../config/config';
-
-const RouterConfig = config.routes;
-
-const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
-
-function formatter(data) {
-  return data
-    .reduce((pre, item) => {
-      pre.push(item.path);
-      return pre;
-    }, [])
-    .filter(item => item);
-}
-
-describe('Homepage', async () => {
-  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));
-  });
-});

+ 0 - 15
e2e/home.e2e.js

@@ -1,15 +0,0 @@
-const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
-
-describe('Homepage', () => {
-  beforeAll(async () => {
-    jest.setTimeout(1000000);
-  });
-  it('it should have logo text', async () => {
-    await page.goto(BASE_URL);
-    await page.waitForSelector('h1', {
-      timeout: 5000,
-    });
-    const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText);
-    expect(text).toContain('Ant Design Pro');
-  });
-});

+ 0 - 18
e2e/topMenu.e2e.js

@@ -1,18 +0,0 @@
-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();
-  });
-});

+ 0 - 34
e2e/userLayout.e2e.js

@@ -1,34 +0,0 @@
-import config from '../config/config';
-
-const RouterConfig = config.routes;
-
-const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
-
-function formatter(data) {
-  return data
-    .reduce((pre, item) => {
-      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);
-  });
-  formatter(RouterConfig[0].routes).forEach(route => {
-    it(`test pages ${route}`, testPage(route));
-  });
-});

mock/notices.js → mock/notices.ts


mock/route.js → mock/route.ts


mock/user.js → mock/user.ts


+ 8 - 5
package.json

@@ -13,8 +13,8 @@
     "analyze": "cross-env ANALYZE=1 umi build",
     "lint:style": "stylelint 'src/**/*.less' --syntax less",
     "lint:prettier": "check-prettier lint",
-    "lint": "eslint --ext .js src mock tests && npm run lint:style && npm run lint:prettier",
-    "lint:fix": "eslint --fix --ext .js src mock tests && stylelint --fix 'src/**/*.less' --syntax less",
+    "lint": "eslint --ext .js src tests && npm run lint:style && npm run lint:prettier",
+    "lint:fix": "eslint --fix --ext .js src tests && stylelint --fix 'src/**/*.less' --syntax less",
     "lint-staged": "lint-staged",
     "lint-staged:js": "eslint --ext .js",
     "test": "umi test",
@@ -47,9 +47,12 @@
     "react-copy-to-clipboard": "^5.0.1",
     "react-document-title": "^2.0.3",
     "react-media": "^1.8.0",
-    "umi-request": "^1.0.0"
+    "react-media-hook2": "^1.0.2",
+    "umi-request": "^1.0.0",
+    "umi-types": "^0.2.0"
   },
   "devDependencies": {
+    "@types/jest": "^24.0.11",
     "@types/react": "^16.8.1",
     "@types/react-dom": "^16.0.11",
     "antd-pro-merge-less": "^1.0.0",
@@ -90,8 +93,8 @@
     "tslint-react": "^3.6.0",
     "umi": "^2.4.4",
     "umi-plugin-ga": "^1.1.3",
-    "umi-plugin-react": "^1.3.4",
-    "umi-plugin-pro-block": "^1.2.0"
+    "umi-plugin-pro-block": "^1.2.0",
+    "umi-plugin-react": "^1.3.4"
   },
   "optionalDependencies": {
     "puppeteer": "^1.12.1"

+ 1 - 1
src/app.js

@@ -26,7 +26,7 @@ export function patchRoutes(routes) {
   Object.keys(authRoutes).map(authKey =>
     ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
   );
-  window.g_routes = routes;
+  (window as any).g_routes = routes;
 }
 
 export function render(oldRender) {

+ 32 - 8
src/components/GlobalHeader/RightContent.js

@@ -1,6 +1,7 @@
-import React, { PureComponent } from 'react';
-import { FormattedMessage, formatMessage } from 'umi/locale';
+import React, { Component } from 'react';
+import { FormattedMessage, formatMessage } from 'umi-plugin-locale';
 import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
+import { ClickParam } from 'antd/es/menu';
 import moment from 'moment';
 import groupBy from 'lodash/groupBy';
 import { NoticeIcon } from 'ant-design-pro';
@@ -9,7 +10,29 @@ import HeaderDropdown from '../HeaderDropdown';
 import SelectLang from '../SelectLang';
 import styles from './index.less';
 
-export default class GlobalHeaderRight extends PureComponent {
+export declare type SiderTheme = 'light' | 'dark';
+
+interface GlobalHeaderRightProps {
+  notices?: any[];
+  dispatch?: (args: any) => void;
+  // wait for https://github.com/umijs/umi/pull/2036
+  currentUser?: {
+    avatar?: string;
+    name?: string;
+    title?: string;
+    group?: string;
+    signature?: string;
+    geographic?: any;
+    tags?: any[];
+    unreadCount: number;
+  };
+  fetchingNotices?: boolean;
+  onNoticeVisibleChange?: (visible: boolean) => void;
+  onMenuClick?: (param: ClickParam) => void;
+  onNoticeClear?: (tabName: string) => void;
+  theme?: SiderTheme;
+}
+export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
   getNoticeData() {
     const { notices = [] } = this.props;
     if (notices.length === 0) {
@@ -41,7 +64,7 @@ export default class GlobalHeaderRight extends PureComponent {
     return groupBy(newNotices, 'type');
   }
 
-  getUnreadData = noticeData => {
+  getUnreadData: (noticeData: object) => any = noticeData => {
     const unreadMsg = {};
     Object.entries(noticeData).forEach(([key, value]) => {
       if (!unreadMsg[key]) {
@@ -126,25 +149,26 @@ export default class GlobalHeaderRight extends PureComponent {
             <Icon type="question-circle-o" />
           </a>
         </Tooltip>
+
         <NoticeIcon
           className={styles.action}
           count={currentUser.unreadCount}
           onItemClick={(item, tabProps) => {
             console.log(item, tabProps); // eslint-disable-line
-            this.changeReadState(item, tabProps);
+            this.changeReadState(item);
           }}
           loading={fetchingNotices}
           locale={{
             emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
             clear: formatMessage({ id: 'component.noticeIcon.clear' }),
-            viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }),
+            viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }), // todo:node_modules/ant-design-pro/lib/NoticeIcon/index.d.ts 21 [key: string]: string;
             notification: formatMessage({ id: 'component.globalHeader.notification' }),
             message: formatMessage({ id: 'component.globalHeader.message' }),
             event: formatMessage({ id: 'component.globalHeader.event' }),
           }}
           onClear={onNoticeClear}
           onPopupVisibleChange={onNoticeVisibleChange}
-          onViewMore={() => message.info('Click on view more')}
+          onViewMore={() => message.info('Click on view more')} // todo:onViewMore?: (tabProps: INoticeIconProps) => void;
           clearClose
         >
           <NoticeIcon.Tab
@@ -153,7 +177,7 @@ export default class GlobalHeaderRight extends PureComponent {
             title="notification"
             emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
             emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
-            showViewMore
+            showViewMore // todo:showViewMore?: boolean;  skeletonProps?: SkeletonProps;
           />
           <NoticeIcon.Tab
             count={unreadMsg.message}

+ 15 - 7
src/components/GlobalHeader/index.js

@@ -1,22 +1,30 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import { Icon } from 'antd';
 import Link from 'umi/link';
-import Debounce from 'lodash-decorators/debounce';
+import debounce from 'lodash/debounce';
 import styles from './index.less';
 import RightContent from './RightContent';
 
-export default class GlobalHeader extends PureComponent {
+interface GlobalHeaderProps {
+  collapsed?: boolean;
+  onCollapse?: (collapsed: boolean) => void;
+  isMobile?: boolean;
+  logo?: string;
+  onNoticeClear?: (type: string) => void;
+  onMenuClick?: ({ key: string }) => void;
+  onNoticeVisibleChange?: (b: boolean) => void;
+}
+
+export default class GlobalHeader extends Component<GlobalHeaderProps> {
   componentWillUnmount() {
     this.triggerResizeEvent.cancel();
   }
-  /* eslint-disable*/
-  @Debounce(600)
-  triggerResizeEvent() {
+  triggerResizeEvent = debounce(() => {
     // eslint-disable-line
     const event = document.createEvent('HTMLEvents');
     event.initEvent('resize', true, false);
     window.dispatchEvent(event);
-  }
+  });
   toggle = () => {
     const { collapsed, onCollapse } = this.props;
     onCollapse(!collapsed);

+ 0 - 2
src/components/HeaderDropdown/index.d.ts

@@ -1,2 +0,0 @@
-import * as React from 'react';
-export default class HeaderDropdown extends React.Component<any, any> {}

+ 0 - 13
src/components/HeaderDropdown/index.js

@@ -1,13 +0,0 @@
-import React, { PureComponent } from 'react';
-import { Dropdown } from 'antd';
-import classNames from 'classnames';
-import styles from './index.less';
-
-export default class HeaderDropdown extends PureComponent {
-  render() {
-    const { overlayClassName, ...props } = this.props;
-    return (
-      <Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
-    );
-  }
-}

+ 22 - 0
src/components/HeaderDropdown/index.tsx

@@ -0,0 +1,22 @@
+import React, { Component } from 'react';
+import { Dropdown } from 'antd';
+import classNames from 'classnames';
+import styles from './index.less';
+
+declare type OverlayFunc = () => React.ReactNode;
+
+interface HeaderDropdownProps {
+  overlayClassName?: string;
+  overlay: React.ReactNode | OverlayFunc;
+  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
+}
+
+export default class HeaderDropdown extends Component<HeaderDropdownProps> {
+  render() {
+    const { overlayClassName, ...props } = this.props;
+
+    return (
+      <Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
+    );
+  }
+}

+ 0 - 15
src/components/HeaderSearch/index.d.ts

@@ -1,15 +0,0 @@
-import * as React from 'react';
-export interface IHeaderSearchProps {
-  placeholder?: string;
-  dataSource?: string[];
-  defaultOpen?: boolean;
-  open?: boolean;
-  onSearch?: (value: string) => void;
-  onChange?: (value: string) => void;
-  onVisibleChange?: (visible: boolean) => void;
-  onPressEnter?: (value: string) => void;
-  style?: React.CSSProperties;
-  className?: string;
-}
-
-export default class HeaderSearch extends React.Component<IHeaderSearchProps, any> {}

+ 22 - 14
src/components/HeaderSearch/index.js

@@ -1,24 +1,30 @@
-import React, { PureComponent } from 'react';
-import PropTypes from 'prop-types';
+import React, { Component } from 'react';
 import { Input, Icon, AutoComplete } from 'antd';
+import InputProps from 'antd/es/input';
+
 import classNames from 'classnames';
 import Debounce from 'lodash-decorators/debounce';
 import Bind from 'lodash-decorators/bind';
 import styles from './index.less';
 
-export default class HeaderSearch extends PureComponent {
-  static propTypes = {
-    className: PropTypes.string,
-    placeholder: PropTypes.string,
-    onSearch: PropTypes.func,
-    onChange: PropTypes.func,
-    onPressEnter: PropTypes.func,
-    defaultActiveFirstOption: PropTypes.bool,
-    dataSource: PropTypes.array,
-    defaultOpen: PropTypes.bool,
-    onVisibleChange: PropTypes.func,
-  };
+interface HeaderSearchProps {
+  onPressEnter: (value: string) => void;
+  onSearch: (value: string) => void;
+  onChange: (value: string) => void;
+  onVisibleChange: (b: boolean) => void;
+  className: string;
+  placeholder: string;
+  defaultActiveFirstOption: boolean;
+  dataSource: any[];
+  defaultOpen: boolean;
+  open?: boolean;
+}
 
+interface HeaderSearchState {
+  value: string;
+  searchMode: boolean;
+}
+export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
   static defaultProps = {
     defaultActiveFirstOption: false,
     onPressEnter: () => {},
@@ -40,6 +46,8 @@ export default class HeaderSearch extends PureComponent {
     return null;
   }
 
+  timeout: NodeJS.Timeout;
+  input: InputProps;
   constructor(props) {
     super(props);
     this.state = {

+ 3 - 1
src/components/IconFont/index.js

@@ -1,6 +1,8 @@
 import { Icon } from 'antd';
-import { iconfontUrl as scriptUrl } from '../../defaultSettings';
+import defaultSettings from '../../../config/defaultSettings';
 
+const { iconfontUrl } = defaultSettings;
+const scriptUrl = iconfontUrl;
 // 使用:
 // import IconFont from '@/components/IconFont';
 // <IconFont type='icon-demo' className='xxx-xxx' />

+ 2 - 1
src/components/PageLoading/index.js

@@ -3,8 +3,9 @@ import { Spin } from 'antd';
 
 // loading components from code split
 // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
-export default () => (
+const PageLoding: React.SFC = () => (
   <div style={{ paddingTop: 100, textAlign: 'center' }}>
     <Spin size="large" />
   </div>
 );
+export default PageLoding;

+ 0 - 49
src/components/SelectLang/index.js

@@ -1,49 +0,0 @@
-import React, { PureComponent } from 'react';
-import { formatMessage, setLocale, getLocale } from 'umi/locale';
-import { Menu, Icon } from 'antd';
-import classNames from 'classnames';
-import HeaderDropdown from '../HeaderDropdown';
-import styles from './index.less';
-
-export default class SelectLang extends PureComponent {
-  changeLang = ({ key }) => {
-    setLocale(key);
-  };
-
-  render() {
-    const { className } = this.props;
-    const selectedLang = getLocale();
-    const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
-    const languageLabels = {
-      'zh-CN': '简体中文',
-      'zh-TW': '繁体中文',
-      'en-US': 'English',
-      'pt-BR': 'Português',
-    };
-    const languageIcons = {
-      'zh-CN': '🇨🇳',
-      'zh-TW': '🇭🇰',
-      'en-US': '🇬🇧',
-      'pt-BR': '🇧🇷',
-    };
-    const langMenu = (
-      <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={this.changeLang}>
-        {locales.map(locale => (
-          <Menu.Item key={locale}>
-            <span role="img" aria-label={languageLabels[locale]}>
-              {languageIcons[locale]}
-            </span>{' '}
-            {languageLabels[locale]}
-          </Menu.Item>
-        ))}
-      </Menu>
-    );
-    return (
-      <HeaderDropdown overlay={langMenu} placement="bottomRight">
-        <span className={classNames(styles.dropDown, className)}>
-          <Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
-        </span>
-      </HeaderDropdown>
-    );
-  }
-}

+ 51 - 0
src/components/SelectLang/index.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale';
+import { Menu, Icon } from 'antd';
+import classNames from 'classnames';
+import HeaderDropdown from '../HeaderDropdown';
+import styles from './index.less';
+
+interface SelectLangProps {
+  className?: string;
+}
+const SelectLang: React.SFC<SelectLangProps> = props => {
+  const { className } = props;
+  const selectedLang = getLocale();
+  const changeLang = ({ key }) => {
+    setLocale(key);
+  };
+  const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
+  const languageLabels = {
+    'zh-CN': '简体中文',
+    'zh-TW': '繁体中文',
+    'en-US': 'English',
+    'pt-BR': 'Português',
+  };
+  const languageIcons = {
+    'zh-CN': '🇨🇳',
+    'zh-TW': '🇭🇰',
+    'en-US': '🇬🇧',
+    'pt-BR': '🇧🇷',
+  };
+  const langMenu = (
+    <Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
+      {locales.map(locale => (
+        <Menu.Item key={locale}>
+          <span role="img" aria-label={languageLabels[locale]}>
+            {languageIcons[locale]}
+          </span>{' '}
+          {languageLabels[locale]}
+        </Menu.Item>
+      ))}
+    </Menu>
+  );
+  return (
+    <HeaderDropdown overlay={langMenu} placement="bottomRight">
+      <span className={classNames(styles.dropDown, className)}>
+        <Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
+      </span>
+    </HeaderDropdown>
+  );
+};
+
+export default SelectLang;

+ 6 - 1
src/components/SettingDrawer/BlockCheckbox.js

@@ -2,7 +2,12 @@ import React from 'react';
 import { Tooltip, Icon } from 'antd';
 import style from './index.less';
 
-const BlockChecbox = ({ value, onChange, list }) => (
+interface BlockChecboxProps {
+  value: string;
+  onChange: (key: string) => void;
+  list: any[];
+}
+const BlockChecbox: React.SFC<BlockChecboxProps> = ({ value, onChange, list }) => (
   <div className={style.blockChecbox} key={value}>
     {list.map(item => (
       <Tooltip title={item.title} key={item.key}>

+ 16 - 3
src/components/SettingDrawer/ThemeColor.js

@@ -1,9 +1,15 @@
 import React from 'react';
 import { Tooltip, Icon } from 'antd';
-import { formatMessage } from 'umi/locale';
+import { formatMessage } from 'umi-plugin-locale';
 import styles from './ThemeColor.less';
 
-const Tag = ({ color, check, ...rest }) => (
+interface TagProps {
+  color: string;
+  check: boolean;
+  className?: string;
+  onClick?: () => void;
+}
+const Tag: React.SFC<TagProps> = ({ color, check, ...rest }) => (
   <div
     {...rest}
     style={{
@@ -14,7 +20,14 @@ const Tag = ({ color, check, ...rest }) => (
   </div>
 );
 
-const ThemeColor = ({ colors, title, value, onChange }) => {
+interface ThemeColorProps {
+  colors?: any[];
+  title?: string;
+  value: string;
+  onChange: (color: string) => void;
+}
+
+const ThemeColor: React.SFC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
   let colorList = colors;
   if (!colors) {
     colorList = [

+ 16 - 5
src/components/SettingDrawer/index.js

@@ -1,16 +1,21 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd';
-import { formatMessage } from 'umi/locale';
+import { formatMessage } from 'umi-plugin-locale';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { connect } from 'dva';
 import omit from 'omit.js';
 import styles from './index.less';
 import ThemeColor from './ThemeColor';
 import BlockCheckbox from './BlockCheckbox';
+import { DefaultSettings } from '../../../config/defaultSettings';
 
 const { Option } = Select;
+interface BodyProps {
+  title: string;
+  style?: React.CSSProperties;
+}
 
-const Body = ({ children, title, style }) => (
+const Body: React.SFC<BodyProps> = ({ children, title, style }) => (
   <div
     style={{
       ...style,
@@ -22,8 +27,14 @@ const Body = ({ children, title, style }) => (
   </div>
 );
 
+interface SettingDrawerProps {
+  setting?: DefaultSettings;
+  dispatch?: (args: any) => void;
+}
+interface SettingDrawerState {}
+
 @connect(({ setting }) => ({ setting }))
-class SettingDrawer extends PureComponent {
+class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
   state = {
     collapse: false,
   };
@@ -120,7 +131,7 @@ class SettingDrawer extends PureComponent {
     return (
       <Tooltip title={item.disabled ? item.disabledReason : ''} placement="left">
         <List.Item actions={[action]}>
-          <span style={{ opacity: item.disabled ? '0.5' : '' }}>{item.title}</span>
+          <span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span>
         </List.Item>
       </Tooltip>
     );

+ 38 - 8
src/components/SiderMenu/BaseMenu.js

@@ -1,12 +1,13 @@
-import React, { PureComponent } from 'react';
+import IconFont from '@/components/IconFont';
+import { isUrl } from '@/utils/utils';
+import { Icon, Menu } from 'antd';
 import classNames from 'classnames';
-import { Menu, Icon } from 'antd';
+import * as H from 'history';
+import React, { Component } from 'react';
 import Link from 'umi/link';
 import { urlToList } from '../_utils/pathTools';
-import { getMenuMatches } from './SiderMenuUtils';
-import { isUrl } from '@/utils/utils';
 import styles from './index.less';
-import IconFont from '@/components/IconFont';
+import { getMenuMatches } from './SiderMenuUtils';
 
 const { SubMenu } = Menu;
 
@@ -28,12 +29,39 @@ const getIcon = icon => {
   return icon;
 };
 
-export default class BaseMenu extends PureComponent {
+export declare type CollapseType = 'clickTrigger' | 'responsive';
+export declare type SiderTheme = 'light' | 'dark';
+export declare type MenuMode =
+  | 'vertical'
+  | 'vertical-left'
+  | 'vertical-right'
+  | 'horizontal'
+  | 'inline';
+
+interface BaseMenuProps {
+  flatMenuKeys?: any[];
+  location?: H.Location;
+  onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
+  isMobile?: boolean;
+  openKeys?: any;
+  theme?: SiderTheme;
+  mode?: MenuMode;
+  className?: string;
+  collapsed?: boolean;
+  handleOpenChange?: (openKeys: any[]) => void;
+  menuData?: any[];
+  style?: React.CSSProperties;
+  onOpenChange?: (openKeys: string[]) => void;
+}
+
+interface BaseMenuState {}
+
+export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
   /**
    * 获得菜单子节点
    * @memberof SiderMenu
    */
-  getNavMenuItems = menusData => {
+  getNavMenuItems: (menusData: any[]) => any[] = menusData => {
     if (!menusData) {
       return [];
     }
@@ -131,6 +159,9 @@ export default class BaseMenu extends PureComponent {
       location: { pathname },
       className,
       collapsed,
+      handleOpenChange,
+      style,
+      menuData,
     } = this.props;
     // if pathname can't match, use the nearest parent's key
     let selectedKeys = this.getSelectedMenuKeys(pathname);
@@ -143,7 +174,6 @@ export default class BaseMenu extends PureComponent {
         openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys,
       };
     }
-    const { handleOpenChange, style, menuData } = this.props;
     const cls = classNames(className, {
       'top-nav-menu': mode === 'horizontal',
     });

+ 0 - 39
src/components/SiderMenu/SiderMenu.test.js

@@ -1,39 +0,0 @@
-import { getFlatMenuKeys } from './SiderMenuUtils';
-
-const menu = [
-  {
-    path: '/dashboard',
-    children: [
-      {
-        path: '/dashboard/name',
-      },
-    ],
-  },
-  {
-    path: '/userinfo',
-    children: [
-      {
-        path: '/userinfo/:id',
-        children: [
-          {
-            path: '/userinfo/:id/info',
-          },
-        ],
-      },
-    ],
-  },
-];
-
-const flatMenuKeys = getFlatMenuKeys(menu);
-
-describe('test convert nested menu to flat menu', () => {
-  it('simple menu', () => {
-    expect(flatMenuKeys).toEqual([
-      '/dashboard',
-      '/dashboard/name',
-      '/userinfo',
-      '/userinfo/:id',
-      '/userinfo/:id/info',
-    ]);
-  });
-});

+ 37 - 17
src/components/SiderMenu/SiderMenu.js

@@ -1,29 +1,39 @@
-import React, { PureComponent, Suspense } from 'react';
 import { Layout } from 'antd';
 import classNames from 'classnames';
+import * as H from 'history';
+import React, { Component, Suspense } from 'react';
 import Link from 'umi/link';
-import styles from './index.less';
+import defaultSettings from '../../../config/defaultSettings';
 import PageLoading from '../PageLoading';
+import styles from './index.less';
 import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
-import { title } from '../../defaultSettings';
 
 const BaseMenu = React.lazy(() => import('./BaseMenu'));
 const { Sider } = Layout;
+const { title } = defaultSettings;
+let firstMount: boolean = true;
 
-let firstMount = true;
+export declare type CollapseType = 'clickTrigger' | 'responsive';
+export declare type SiderTheme = 'light' | 'dark';
 
-export default class SiderMenu extends PureComponent {
-  constructor(props) {
-    super(props);
-    this.state = {
-      openKeys: getDefaultCollapsedSubMenus(props),
-    };
-  }
+interface SiderMenuProps {
+  menuData: any[];
+  location?: H.Location;
+  flatMenuKeys?: any[];
+  logo?: string;
+  collapsed: boolean;
+  onCollapse: (collapsed: boolean, type?: CollapseType) => void;
+  fixSiderbar?: boolean;
+  theme?: SiderTheme;
+  isMobile: boolean;
+}
 
-  componentDidMount() {
-    firstMount = false;
-  }
+interface SiderMenuState {
+  openKeys: any;
+  flatMenuKeysLen?: number;
+}
 
+export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> {
   static getDerivedStateFromProps(props, state) {
     const { pathname, flatMenuKeysLen } = state;
     if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
@@ -35,8 +45,18 @@ export default class SiderMenu extends PureComponent {
     }
     return null;
   }
+  constructor(props: SiderMenuProps) {
+    super(props);
+    this.state = {
+      openKeys: getDefaultCollapsedSubMenus(props),
+    };
+  }
+
+  componentDidMount() {
+    firstMount = false;
+  }
 
-  isMainMenu = key => {
+  isMainMenu: (key: string) => boolean = key => {
     const { menuData } = this.props;
     return menuData.some(item => {
       if (key) {
@@ -46,7 +66,7 @@ export default class SiderMenu extends PureComponent {
     });
   };
 
-  handleOpenChange = openKeys => {
+  handleOpenChange: (openKeys: any[]) => void = openKeys => {
     const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
     this.setState({
       openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
@@ -65,7 +85,7 @@ export default class SiderMenu extends PureComponent {
     return (
       <Sider
         trigger={null}
-        collapsible
+        collapsible={true}
         collapsed={collapsed}
         breakpoint="lg"
         onCollapse={collapse => {

+ 1 - 0
src/components/SiderMenu/SiderMenuUtils.js

@@ -24,6 +24,7 @@ export const getMenuMatches = (flatMenuKeys, path) =>
     }
     return false;
   });
+
 /**
  * 获得菜单子节点
  * @memberof SiderMenu

+ 14 - 3
src/components/SiderMenu/index.js

@@ -3,7 +3,18 @@ import { Drawer } from 'antd';
 import SiderMenu from './SiderMenu';
 import { getFlatMenuKeys } from './SiderMenuUtils';
 
-const SiderMenuWrapper = React.memo(props => {
+export declare type SiderTheme = 'light' | 'dark';
+
+interface SiderMenuProps {
+  isMobile: boolean;
+  menuData: any[];
+  collapsed: boolean;
+  logo?: string;
+  theme?: SiderTheme;
+  onCollapse: (payload: boolean) => void;
+}
+
+const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
   const { isMobile, menuData, collapsed, onCollapse } = props;
   const flatMenuKeys = getFlatMenuKeys(menuData);
   return isMobile ? (
@@ -21,6 +32,6 @@ const SiderMenuWrapper = React.memo(props => {
   ) : (
     <SiderMenu {...props} flatMenuKeys={flatMenuKeys} />
   );
-});
+};
 
-export default SiderMenuWrapper;
+export default React.memo(SiderMenuWrapper);

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

@@ -1,16 +1,52 @@
-import React, { PureComponent } from 'react';
+import React, { Component } from 'react';
 import Link from 'umi/link';
 import RightContent from '../GlobalHeader/RightContent';
 import BaseMenu from '../SiderMenu/BaseMenu';
 import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
 import styles from './index.less';
-import { title } from '../../defaultSettings';
+import defaultSettings from '../../../config/defaultSettings';
 
-export default class TopNavHeader extends PureComponent {
+export declare type CollapseType = 'clickTrigger' | 'responsive';
+export declare type SiderTheme = 'light' | 'dark';
+export declare type MenuMode =
+  | 'vertical'
+  | 'vertical-left'
+  | 'vertical-right'
+  | 'horizontal'
+  | 'inline';
+
+const { title } = defaultSettings;
+interface TopNavHeaderProps {
+  theme: SiderTheme;
+  contentWidth?: string;
+  menuData?: any[];
+  logo?: string;
+  mode?: MenuMode;
+  flatMenuKeys?: any[];
+  onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
+  isMobile?: boolean;
+  openKeys?: any;
+  className?: string;
+  collapsed?: boolean;
+  handleOpenChange?: (openKeys: any[]) => void;
+  style?: React.CSSProperties;
+  onOpenChange?: (openKeys: string[]) => void;
+  onNoticeClear?: (type: string) => void;
+  onMenuClick?: ({ key: string }) => void;
+  onNoticeVisibleChange?: (b: boolean) => void;
+}
+
+interface TopNavHeaderState {
+  maxWidth: undefined | number;
+}
+
+export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
   state = {
     maxWidth: undefined,
   };
 
+  maim: HTMLDivElement;
+
   static getDerivedStateFromProps(props) {
     return {
       maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40,

+ 0 - 17
src/components/_utils/pathTools.test.js

@@ -1,17 +0,0 @@
-import { urlToList } from './pathTools';
-
-describe('test urlToList', () => {
-  it('A path', () => {
-    expect(urlToList('/userinfo')).toEqual(['/userinfo']);
-  });
-  it('Secondary path', () => {
-    expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']);
-  });
-  it('Three paths', () => {
-    expect(urlToList('/userinfo/2144/addr')).toEqual([
-      '/userinfo',
-      '/userinfo/2144',
-      '/userinfo/2144/addr',
-    ]);
-  });
-});

src/components/_utils/pathTools.js → src/components/_utils/pathTools.ts


+ 4 - 4
src/global.js

@@ -1,9 +1,9 @@
 import React from 'react';
 import { notification, Button, message } from 'antd';
-import { formatMessage } from 'umi/locale';
-import defaultSettings from './defaultSettings';
+import { formatMessage } from 'umi-plugin-locale';
+import defaultSettings from '../config/defaultSettings';
 
-window.React = React;
+(window as any).React = React;
 
 const { pwa } = defaultSettings;
 // if pwa is true
@@ -14,7 +14,7 @@ if (pwa) {
   });
 
   // Pop up a prompt on the page asking the user if they want to use the latest version
-  window.addEventListener('sw.updated', e => {
+  window.addEventListener('sw.updated', (e: CustomEvent) => {
     const reloadSW = async () => {
       // Check if there is sw whose state is waiting in ServiceWorkerRegistration
       // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration

+ 0 - 173
src/layouts/BasicLayout.js

@@ -1,173 +0,0 @@
-import React, { Suspense } from 'react';
-import { Layout } from 'antd';
-import DocumentTitle from 'react-document-title';
-import { connect } from 'dva';
-import { ContainerQuery } from 'react-container-query';
-import classNames from 'classnames';
-import Media from 'react-media';
-import logo from '../assets/logo.svg';
-import Footer from './Footer';
-import Header from './Header';
-import Context from './MenuContext';
-import PageLoading from '@/components/PageLoading';
-import SiderMenu from '@/components/SiderMenu';
-import getPageTitle from '@/utils/getPageTitle';
-import styles from './BasicLayout.less';
-
-// lazy load SettingDrawer
-const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
-
-const { Content } = Layout;
-
-const query = {
-  'screen-xs': {
-    maxWidth: 575,
-  },
-  'screen-sm': {
-    minWidth: 576,
-    maxWidth: 767,
-  },
-  'screen-md': {
-    minWidth: 768,
-    maxWidth: 991,
-  },
-  'screen-lg': {
-    minWidth: 992,
-    maxWidth: 1199,
-  },
-  'screen-xl': {
-    minWidth: 1200,
-    maxWidth: 1599,
-  },
-  'screen-xxl': {
-    minWidth: 1600,
-  },
-};
-
-class BasicLayout extends React.Component {
-  componentDidMount() {
-    const {
-      dispatch,
-      route: { routes, authority },
-    } = this.props;
-    dispatch({
-      type: 'user/fetchCurrent',
-    });
-    dispatch({
-      type: 'setting/getSetting',
-    });
-    dispatch({
-      type: 'menu/getMenuData',
-      payload: { routes, authority },
-    });
-  }
-
-  getContext() {
-    const { location, breadcrumbNameMap } = this.props;
-    return {
-      location,
-      breadcrumbNameMap,
-    };
-  }
-
-  getLayoutStyle = () => {
-    const { fixSiderbar, isMobile, collapsed, layout } = this.props;
-    if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
-      return {
-        paddingLeft: collapsed ? '80px' : '256px',
-      };
-    }
-    return null;
-  };
-
-  handleMenuCollapse = collapsed => {
-    const { dispatch } = this.props;
-    dispatch({
-      type: 'global/changeLayoutCollapsed',
-      payload: collapsed,
-    });
-  };
-
-  renderSettingDrawer = () => {
-    // Do not render SettingDrawer in production
-    // unless it is deployed in preview.pro.ant.design as demo
-    if (process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') {
-      return null;
-    }
-    return <SettingDrawer />;
-  };
-
-  render() {
-    const {
-      navTheme,
-      layout: PropsLayout,
-      children,
-      location: { pathname },
-      isMobile,
-      menuData,
-      breadcrumbNameMap,
-      fixedHeader,
-    } = this.props;
-
-    const isTop = PropsLayout === 'topmenu';
-    const contentStyle = !fixedHeader ? { paddingTop: 0 } : {};
-    const layout = (
-      <Layout>
-        {isTop && !isMobile ? null : (
-          <SiderMenu
-            logo={logo}
-            theme={navTheme}
-            onCollapse={this.handleMenuCollapse}
-            menuData={menuData}
-            isMobile={isMobile}
-            {...this.props}
-          />
-        )}
-        <Layout
-          style={{
-            ...this.getLayoutStyle(),
-            minHeight: '100vh',
-          }}
-        >
-          <Header
-            menuData={menuData}
-            handleMenuCollapse={this.handleMenuCollapse}
-            logo={logo}
-            isMobile={isMobile}
-            {...this.props}
-          />
-          <Content className={styles.content} style={contentStyle}>
-            {children}
-          </Content>
-          <Footer />
-        </Layout>
-      </Layout>
-    );
-    return (
-      <React.Fragment>
-        <DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}>
-          <ContainerQuery query={query}>
-            {params => (
-              <Context.Provider value={this.getContext()}>
-                <div className={classNames(params)}>{layout}</div>
-              </Context.Provider>
-            )}
-          </ContainerQuery>
-        </DocumentTitle>
-        <Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense>
-      </React.Fragment>
-    );
-  }
-}
-
-export default connect(({ global, setting, menu: menuModel }) => ({
-  collapsed: global.collapsed,
-  layout: setting.layout,
-  menuData: menuModel.menuData,
-  breadcrumbNameMap: menuModel.breadcrumbNameMap,
-  ...setting,
-}))(props => (
-  <Media query="(max-width: 599px)">
-    {isMobile => <BasicLayout {...props} isMobile={isMobile} />}
-  </Media>
-));

+ 153 - 0
src/layouts/BasicLayout.tsx

@@ -0,0 +1,153 @@
+import PageLoading from '@/components/PageLoading';
+import SiderMenu from '@/components/SiderMenu';
+import getPageTitle from '@/utils/getPageTitle';
+import { Layout } from 'antd';
+import classNames from 'classnames';
+import { connect } from 'dva';
+import React, { Suspense, useState } from 'react';
+import { ContainerQuery } from 'react-container-query';
+import DocumentTitle from 'react-document-title';
+import useMedia from 'react-media-hook2';
+import logo from '../assets/logo.svg';
+import styles from './BasicLayout.less';
+import Footer from './Footer';
+import Header from './Header';
+import Context from './MenuContext';
+
+// lazy load SettingDrawer
+const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
+
+const { Content } = Layout;
+
+const query = {
+  'screen-xs': {
+    maxWidth: 575,
+  },
+  'screen-sm': {
+    minWidth: 576,
+    maxWidth: 767,
+  },
+  'screen-md': {
+    minWidth: 768,
+    maxWidth: 991,
+  },
+  'screen-lg': {
+    minWidth: 992,
+    maxWidth: 1199,
+  },
+  'screen-xl': {
+    minWidth: 1200,
+    maxWidth: 1599,
+  },
+  'screen-xxl': {
+    minWidth: 1600,
+  },
+};
+
+export declare type SiderTheme = 'light' | 'dark';
+
+interface BasicLayoutProps {
+  dispatch: (args: any) => void;
+  // wait for https://github.com/umijs/umi/pull/2036
+  route: any;
+  breadcrumbNameMap: object;
+  fixSiderbar: boolean;
+  layout: string;
+  navTheme: SiderTheme;
+  menuData: any[];
+  fixedHeader: boolean;
+  location: Location;
+  collapsed: boolean;
+}
+
+interface BasicLayoutContext {
+  location: Location;
+  breadcrumbNameMap: object;
+}
+
+const BasicLayout: React.SFC<BasicLayoutProps> = props => {
+  const {
+    breadcrumbNameMap,
+    dispatch,
+    children,
+    collapsed,
+    fixedHeader,
+    fixSiderbar,
+    layout: PropsLayout,
+    location,
+    menuData,
+    navTheme,
+    route: { routes, authority },
+  } = props;
+  useState(() => {
+    dispatch({ type: 'user/fetchCurrent' });
+    dispatch({ type: 'setting/getSetting' });
+    dispatch({ type: 'menu/getMenuData', payload: { routes, authority } });
+  });
+  const isTop = PropsLayout === 'topmenu';
+  const contentStyle = !fixedHeader ? { paddingTop: 0 } : {};
+  const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0];
+  const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
+  const getContext = (): BasicLayoutContext => ({ location, breadcrumbNameMap });
+  const handleMenuCollapse = (payload: boolean) =>
+    dispatch({ type: 'global/changeLayoutCollapsed', payload });
+  // Do not render SettingDrawer in production
+  // unless it is deployed in preview.pro.ant.design as demo
+  const renderSettingDrawer = () =>
+    !(process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') && <SettingDrawer />;
+
+  const layout = (
+    <Layout>
+      {isTop && !isMobile ? null : (
+        <SiderMenu
+          logo={logo}
+          theme={navTheme}
+          onCollapse={handleMenuCollapse}
+          menuData={menuData}
+          isMobile={isMobile}
+          {...props}
+        />
+      )}
+      <Layout
+        style={{
+          paddingLeft: hasLeftPadding ? (collapsed ? 80 : 256) : void 0,
+          minHeight: '100vh',
+        }}
+      >
+        <Header
+          menuData={menuData}
+          handleMenuCollapse={handleMenuCollapse}
+          logo={logo}
+          isMobile={isMobile}
+          {...props}
+        />
+        <Content className={styles.content} style={contentStyle}>
+          {children}
+        </Content>
+        <Footer />
+      </Layout>
+    </Layout>
+  );
+  return (
+    <React.Fragment>
+      <DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}>
+        <ContainerQuery query={query}>
+          {params => (
+            <Context.Provider value={getContext()}>
+              <div className={classNames(params)}>{layout}</div>
+            </Context.Provider>
+          )}
+        </ContainerQuery>
+      </DocumentTitle>
+      <Suspense fallback={<PageLoading />}>{renderSettingDrawer()}</Suspense>
+    </React.Fragment>
+  );
+};
+
+export default connect(({ global, setting, menu: menuModel }) => ({
+  collapsed: global.collapsed,
+  layout: setting.layout,
+  menuData: menuModel.menuData,
+  breadcrumbNameMap: menuModel.breadcrumbNameMap,
+  ...setting,
+}))(BasicLayout);

+ 0 - 3
src/layouts/BlankLayout.js

@@ -1,3 +0,0 @@
-import React from 'react';
-
-export default ({ children }) => <div>{children}</div>;

+ 9 - 0
src/layouts/BlankLayout.tsx

@@ -0,0 +1,9 @@
+import React, { ReactNode, SFC } from 'react';
+
+interface BlankLayoutProps {
+  children: ReactNode;
+}
+
+const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
+
+export default Layout;

+ 1 - 1
src/layouts/Footer.js

@@ -1,5 +1,5 @@
+import { Icon, Layout } from 'antd';
 import React, { Fragment } from 'react';
-import { Layout, Icon } from 'antd';
 import { GlobalFooter } from 'ant-design-pro';
 
 const { Footer } = Layout;

+ 31 - 11
src/layouts/Header.js

@@ -1,21 +1,33 @@
-import React, { Component } from 'react';
-import { formatMessage } from 'umi/locale';
+import GlobalHeader from '@/components/GlobalHeader';
+import TopNavHeader from '@/components/TopNavHeader';
+import { DefaultSettings } from '../../config/defaultSettings';
 import { Layout, message } from 'antd';
-import Animate from 'rc-animate';
 import { connect } from 'dva';
+import Animate from 'rc-animate';
+import React, { Component } from 'react';
+import { formatMessage } from 'umi-plugin-locale';
 import router from 'umi/router';
-import GlobalHeader from '@/components/GlobalHeader';
-import TopNavHeader from '@/components/TopNavHeader';
 import styles from './Header.less';
 
 const { Header } = Layout;
 
-class HeaderView extends Component {
-  state = {
-    visible: true,
-  };
+export declare type SiderTheme = 'light' | 'dark';
 
-  static getDerivedStateFromProps(props, state) {
+interface HeaderViewProps {
+  isMobile: boolean;
+  collapsed: boolean;
+  setting: DefaultSettings;
+  dispatch: (args: any) => void;
+  autoHideHeader: boolean;
+  handleMenuCollapse: (args: boolean) => void;
+}
+
+interface HeaderViewState {
+  visible: boolean;
+}
+
+class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
+  static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) {
     if (!props.autoHideHeader && !state.visible) {
       return {
         visible: true,
@@ -24,6 +36,14 @@ class HeaderView extends Component {
     return null;
   }
 
+  state = {
+    visible: true,
+  };
+
+  ticking: boolean;
+
+  oldScrollTop: number;
+
   componentDidMount() {
     document.addEventListener('scroll', this.handScroll, { passive: true });
   }
@@ -123,7 +143,7 @@ class HeaderView extends Component {
       <Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}>
         {isTop && !isMobile ? (
           <TopNavHeader
-            theme={navTheme}
+            theme={navTheme as SiderTheme}
             mode="horizontal"
             onCollapse={handleMenuCollapse}
             onNoticeClear={this.handleNoticeClear}

+ 1 - 1
src/layouts/MenuContext.js

@@ -1,3 +1,3 @@
 import { createContext } from 'react';
 
-export default createContext();
+export default createContext({});

+ 17 - 9
src/layouts/UserLayout.js

@@ -1,14 +1,14 @@
-import React, { Component, Fragment } from 'react';
-import { formatMessage } from 'umi/locale';
-import { connect } from 'dva';
-import Link from 'umi/link';
-import { Icon } from 'antd';
+import SelectLang from '@/components/SelectLang';
+import getPageTitle from '@/utils/getPageTitle';
 import { GlobalFooter } from 'ant-design-pro';
+import { Icon } from 'antd';
+import { connect } from 'dva';
+import React, { Component, Fragment } from 'react';
 import DocumentTitle from 'react-document-title';
-import SelectLang from '@/components/SelectLang';
-import styles from './UserLayout.less';
+import { formatMessage } from 'umi-plugin-locale';
+import Link from 'umi/link';
 import logo from '../assets/logo.svg';
-import getPageTitle from '@/utils/getPageTitle';
+import styles from './UserLayout.less';
 
 const links = [
   {
@@ -34,7 +34,15 @@ const copyright = (
   </Fragment>
 );
 
-class UserLayout extends Component {
+interface UserLayoutProps {
+  dispatch: (args: any) => void;
+  route: any;
+  breadcrumbNameMap: object;
+  navTheme: string;
+  location: Location;
+}
+
+class UserLayout extends Component<UserLayoutProps> {
   componentDidMount() {
     const {
       dispatch,

src/locales/en-US.js → src/locales/en-US.ts


src/locales/en-US/component.js → src/locales/en-US/component.ts


src/locales/en-US/globalHeader.js → src/locales/en-US/globalHeader.ts


src/locales/en-US/menu.js → src/locales/en-US/menu.ts


src/locales/en-US/pwa.js → src/locales/en-US/pwa.ts


src/locales/en-US/settingDrawer.js → src/locales/en-US/settingDrawer.ts


src/locales/en-US/settings.js → src/locales/en-US/settings.ts


src/locales/pt-BR.js → src/locales/pt-BR.ts


src/locales/pt-BR/component.js → src/locales/pt-BR/component.ts


src/locales/pt-BR/globalHeader.js → src/locales/pt-BR/globalHeader.ts


src/locales/pt-BR/menu.js → src/locales/pt-BR/menu.ts


src/locales/pt-BR/pwa.js → src/locales/pt-BR/pwa.ts


src/locales/pt-BR/settingDrawer.js → src/locales/pt-BR/settingDrawer.ts


src/locales/pt-BR/settings.js → src/locales/pt-BR/settings.ts


src/locales/zh-CN.js → src/locales/zh-CN.ts


src/locales/zh-CN/component.js → src/locales/zh-CN/component.ts


src/locales/zh-CN/globalHeader.js → src/locales/zh-CN/globalHeader.ts


src/locales/zh-CN/menu.js → src/locales/zh-CN/menu.ts


src/locales/zh-CN/pwa.js → src/locales/zh-CN/pwa.ts


src/locales/zh-CN/settingDrawer.js → src/locales/zh-CN/settingDrawer.ts


src/locales/zh-CN/settings.js → src/locales/zh-CN/settings.ts


src/locales/zh-TW.js → src/locales/zh-TW.ts


src/locales/zh-TW/component.js → src/locales/zh-TW/component.ts


src/locales/zh-TW/globalHeader.js → src/locales/zh-TW/globalHeader.ts


src/locales/zh-TW/menu.js → src/locales/zh-TW/menu.ts


src/locales/zh-TW/pwa.js → src/locales/zh-TW/pwa.ts


src/locales/zh-TW/settingDrawer.js → src/locales/zh-TW/settingDrawer.ts


src/locales/zh-TW/settings.js → src/locales/zh-TW/settings.ts


+ 28 - 3
src/models/global.js

@@ -1,6 +1,29 @@
 import { queryNotices } from '@/services/user';
+import { Effect, Subscription } from 'dva';
+import { Reducer } from 'redux';
 
-export default {
+export interface GlobalModelState {
+  collapsed: boolean;
+  notices: any[];
+}
+
+export interface GlobalModelType {
+  namespace: 'global';
+  state: GlobalModelState;
+  effects: {
+    fetchNotices: Effect;
+    clearNotices: Effect;
+    changeNoticeReadState: Effect;
+  };
+  reducers: {
+    changeLayoutCollapsed: Reducer<any>;
+    saveNotices: Reducer<any>;
+    saveClearedNotices: Reducer<any>;
+  };
+  subscriptions: { setup: Subscription };
+}
+
+const GlobalModel: GlobalModelType = {
   namespace: 'global',
 
   state: {
@@ -92,10 +115,12 @@ export default {
     setup({ history }) {
       // Subscribe history(url) change, trigger `load` action if pathname is `/`
       return history.listen(({ pathname, search }) => {
-        if (typeof window.ga !== 'undefined') {
-          window.ga('send', 'pageview', pathname + search);
+        if (typeof (window as any).ga !== 'undefined') {
+          (window as any).ga('send', 'pageview', pathname + search);
         }
       });
     },
   },
 };
+
+export default GlobalModel;

+ 42 - 13
src/models/menu.js

@@ -1,13 +1,15 @@
-import memoizeOne from 'memoize-one';
-import isEqual from 'lodash/isEqual';
-import { formatMessage } from 'umi/locale';
 import Authorized from '@/utils/Authorized';
-import { menu } from '../defaultSettings';
-
+import { Effect } from 'dva';
+import isEqual from 'lodash/isEqual';
+import memoizeOne from 'memoize-one';
+import { Reducer } from 'redux';
+import { formatMessage } from 'umi-plugin-locale';
+import defaultSettings from '../../config/defaultSettings';
+const { menu } = defaultSettings;
 const { check } = Authorized;
 
 // Conversion router to menu.
-function formatter(data, parentAuthority, parentName) {
+function formatter(data: any[], parentAuthority: string[], parentName: string): any[] {
   return data
     .map(item => {
       if (!item.name || !item.path) {
@@ -44,10 +46,19 @@ function formatter(data, parentAuthority, parentName) {
 
 const memoizeOneFormatter = memoizeOne(formatter, isEqual);
 
+interface SubMenuItem {
+  children: SubMenuItem[];
+  hideChildrenInMenu?: boolean;
+  hideInMenu?: boolean;
+  name?: any;
+  component: any;
+  authority?: string[];
+  path: string;
+}
 /**
  * get SubMenu or Item
  */
-const getSubMenu = item => {
+const getSubMenu: (item: SubMenuItem) => any = item => {
   // doc: add hideChildrenInMenu
   if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
     return {
@@ -61,23 +72,23 @@ const getSubMenu = item => {
 /**
  * filter menuData
  */
-const filterMenuData = menuData => {
+const filterMenuData: (menuData: SubMenuItem[]) => SubMenuItem[] = menuData => {
   if (!menuData) {
     return [];
   }
   return menuData
     .filter(item => item.name && !item.hideInMenu)
-    .map(item => check(item.authority, getSubMenu(item)))
+    .map(item => check(item.authority, getSubMenu(item), null))
     .filter(item => item);
 };
 /**
  * 获取面包屑映射
- * @param {Object} menuData 菜单配置
+ * @param ISubMenuItem[] menuData 菜单配置
  */
-const getBreadcrumbNameMap = menuData => {
+const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
   const routerMap = {};
 
-  const flattenMenuData = data => {
+  const flattenMenuData: (data: SubMenuItem[]) => void = data => {
     data.forEach(menuItem => {
       if (menuItem.children) {
         flattenMenuData(menuItem.children);
@@ -92,7 +103,23 @@ const getBreadcrumbNameMap = menuData => {
 
 const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
 
-export default {
+export interface MenuModelState {
+  menuData: any[];
+  routerData: any[];
+  breadcrumbNameMap: object;
+}
+
+export interface MenuModelType {
+  namespace: 'menu';
+  state: MenuModelState;
+  effects: {
+    getMenuData: Effect;
+  };
+  reducers: {
+    save: Reducer<any>;
+  };
+}
+const MenuModel: MenuModelType = {
   namespace: 'menu',
 
   state: {
@@ -123,3 +150,5 @@ export default {
     },
   },
 };
+
+export default MenuModel;

+ 20 - 8
src/models/setting.js

@@ -1,8 +1,18 @@
 import { message } from 'antd';
-import defaultSettings from '../defaultSettings';
+import { Reducer } from 'redux';
+import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
 
-let lessNodesAppended;
-const updateTheme = primaryColor => {
+export interface SettingModelType {
+  namespace: 'setting';
+  state: DefaultSettings;
+  reducers: {
+    getSetting: Reducer<any>;
+    changeSetting: Reducer<any>;
+  };
+}
+let lessNodesAppended: boolean;
+
+const updateTheme: (primaryColor?: string) => void = primaryColor => {
   // Don't compile less in production!
   if (APP_TYPE !== 'site') {
     return;
@@ -13,11 +23,12 @@ const updateTheme = primaryColor => {
   }
   const hideMessage = message.loading('正在编译主题!', 0);
   function buildIt() {
-    if (!window.less) {
+    if (!(window as any).less) {
+      console.log('no less');
       return;
     }
     setTimeout(() => {
-      window.less
+      (window as any).less
         .modifyVars({
           '@primary-color': primaryColor,
         })
@@ -59,16 +70,16 @@ const updateTheme = primaryColor => {
   }
 };
 
-const updateColorWeak = colorWeak => {
+const updateColorWeak: (colorWeak: string) => void = colorWeak => {
   document.body.className = colorWeak ? 'colorWeak' : '';
 };
 
-export default {
+const SettingModel: SettingModelType = {
   namespace: 'setting',
   state: defaultSettings,
   reducers: {
     getSetting(state) {
-      const setting = {};
+      const setting: any = {};
       const urlParams = new URL(window.location.href);
       Object.keys(state).forEach(key => {
         if (urlParams.searchParams.has(key)) {
@@ -121,3 +132,4 @@ export default {
     },
   },
 };
+export default SettingModel;

+ 33 - 1
src/models/user.js

@@ -1,6 +1,36 @@
 import { query as queryUsers, queryCurrent } from '@/services/user';
+import { Effect } from 'dva';
+import { Reducer } from 'redux';
 
-export default {
+export interface UserModelState {
+  list: any[];
+  currentUser: {
+    avatar?: string;
+    name?: string;
+    title?: string;
+    group?: string;
+    signature?: string;
+    geographic?: any;
+    tags?: any[];
+    unreadCount?: number;
+  };
+}
+
+export interface UserModelType {
+  namespace: 'user';
+  state: UserModelState;
+  effects: {
+    fetch: Effect;
+    fetchCurrent: Effect;
+  };
+  reducers: {
+    save: Reducer<any>;
+    saveCurrentUser: Reducer<any>;
+    changeNotifyCount: Reducer<any>;
+  };
+}
+
+const UserModel: UserModelType = {
   namespace: 'user',
 
   state: {
@@ -50,3 +80,5 @@ export default {
     },
   },
 };
+
+export default UserModel;

+ 0 - 44
src/pages/Authorized.js

@@ -1,44 +0,0 @@
-import React from 'react';
-import Redirect from 'umi/redirect';
-import pathToRegexp from 'path-to-regexp';
-import { connect } from 'dva';
-import Authorized from '@/utils/Authorized';
-
-function AuthComponent({ children, location, routerData, currentCuser }) {
-  const isLogin = currentCuser && currentCuser.name;
-
-  const getRouteAuthority = (pathname, routeData) => {
-    const routes = routeData.slice(); // clone
-
-    const getAuthority = (routeDatas, path) => {
-      let authorities;
-      routeDatas.forEach(route => {
-        // check partial route
-        if (pathToRegexp(`${route.path}(.*)`).test(path)) {
-          if (route.authority) {
-            authorities = route.authority;
-          }
-          // is exact route?
-          if (!pathToRegexp(route.path).test(path) && route.routes) {
-            authorities = getAuthority(route.routes, path);
-          }
-        }
-      });
-      return authorities;
-    };
-
-    return getAuthority(routes, pathname);
-  };
-  return (
-    <Authorized
-      authority={getRouteAuthority(location.pathname, routerData)}
-      noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
-    >
-      {children}
-    </Authorized>
-  );
-}
-export default connect(({ menu: menuModel, user: userModel }) => ({
-  routerData: menuModel.routerData,
-  currentCuser: userModel.currentCuser,
-}))(AuthComponent);

+ 45 - 0
src/pages/Authorized.tsx

@@ -0,0 +1,45 @@
+import Authorized from '@/utils/Authorized';
+import { connect } from 'dva';
+import pathToRegexp from 'path-to-regexp';
+import React from 'react';
+import Redirect from 'umi/redirect';
+import { UserModelState } from '../models/user';
+
+interface AuthComponentProps {
+  location: Location;
+  routerData: any[];
+  user: UserModelState;
+}
+
+const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, routerData, user }) => {
+  const { currentUser } = user;
+  const isLogin = currentUser && currentUser.name;
+  const getRouteAuthority = (path, routeData) => {
+    let authorities;
+    routeData.forEach(route => {
+      // match prefix
+      if (pathToRegexp(`${route.path}(.*)`).test(path)) {
+        authorities = route.authority || authorities;
+
+        // get children authority recursively
+        if (route.routes) {
+          authorities = getRouteAuthority(path, route.routes) || authorities;
+        }
+      }
+    });
+    return authorities;
+  };
+  return (
+    <Authorized
+      authority={getRouteAuthority(location.pathname, routerData)}
+      noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
+    >
+      {children}
+    </Authorized>
+  );
+};
+
+export default connect(({ menu: menuModel, user }) => ({
+  routerData: menuModel.routerData,
+  user,
+}))(AuthComponent);

src/pages/Welcome.js → src/pages/Welcome.tsx


+ 3 - 3
src/services/user.js

@@ -1,13 +1,13 @@
 import request from '@/utils/request';
 
-export async function query() {
+export async function query(): Promise<any> {
   return request('/api/users');
 }
 
-export async function queryCurrent() {
+export async function queryCurrent(): Promise<any> {
   return request('/api/currentUser');
 }
 
-export async function queryNotices() {
+export async function queryNotices(): Promise<any> {
   return request('/api/notices');
 }

+ 12 - 0
src/typings.d.ts

@@ -0,0 +1,12 @@
+declare module '*.css';
+declare module '*.less';
+declare module '*.scss';
+declare module '*.sass';
+declare module '*.svg';
+declare module '*.png';
+declare module '*.jpg';
+declare module '*.jpeg';
+declare module '*.gif';
+declare module '*.bmp';
+declare module '*.tiff';
+declare var APP_TYPE: string;

src/utils/Authorized.js → src/utils/Authorized.ts


+ 1 - 0
src/utils/authority.test.js

@@ -1,3 +1,4 @@
+import 'jest';
 import { getAuthority } from './authority';
 
 describe('getAuthority should be strong', () => {

+ 2 - 2
src/utils/authority.js

@@ -1,5 +1,5 @@
 // use localStorage to store the authority info, which might be sent from server in actual project.
-export function getAuthority(str) {
+export function getAuthority(str?: string): any {
   // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
   const authorityString =
     typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str;
@@ -16,7 +16,7 @@ export function getAuthority(str) {
   return authority || ['admin'];
 }
 
-export function setAuthority(authority) {
+export function setAuthority(authority: string | string[]): void {
   const proAuthority = typeof authority === 'string' ? [authority] : authority;
   return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
 }

+ 16 - 5
src/utils/getPageTitle.js

@@ -1,15 +1,26 @@
-import { formatMessage } from 'umi/locale';
-import pathToRegexp from 'path-to-regexp';
 import isEqual from 'lodash/isEqual';
 import memoizeOne from 'memoize-one';
-import { menu, title } from '../defaultSettings';
+import pathToRegexp from 'path-to-regexp';
+import { formatMessage } from 'umi-plugin-locale';
+import defaultSettings from '../../config/defaultSettings';
+
+const { menu, title } = defaultSettings;
+
+interface RouterData {
+  name: string;
+  locale: string;
+  authority?: string[];
+  children?: any[];
+  icon?: string;
+  path: string;
+}
 
-export const matchParamsPath = (pathname, breadcrumbNameMap) => {
+export const matchParamsPath = (pathname: string, breadcrumbNameMap: object): RouterData => {
   const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname));
   return breadcrumbNameMap[pathKey];
 };
 
-const getPageTitle = (pathname, breadcrumbNameMap) => {
+const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
   const currRouterData = matchParamsPath(pathname, breadcrumbNameMap);
   if (!currRouterData) {
     return title;

src/utils/request.js → src/utils/request.ts


+ 0 - 38
src/utils/utils.test.js

@@ -1,38 +0,0 @@
-import { isUrl } from './utils';
-
-describe('isUrl tests', () => {
-  it('should return false for invalid and corner case inputs', () => {
-    expect(isUrl([])).toBeFalsy();
-    expect(isUrl({})).toBeFalsy();
-    expect(isUrl(false)).toBeFalsy();
-    expect(isUrl(true)).toBeFalsy();
-    expect(isUrl(NaN)).toBeFalsy();
-    expect(isUrl(null)).toBeFalsy();
-    expect(isUrl(undefined)).toBeFalsy();
-    expect(isUrl()).toBeFalsy();
-    expect(isUrl('')).toBeFalsy();
-  });
-
-  it('should return false for invalid URLs', () => {
-    expect(isUrl('foo')).toBeFalsy();
-    expect(isUrl('bar')).toBeFalsy();
-    expect(isUrl('bar/test')).toBeFalsy();
-    expect(isUrl('http:/example.com/')).toBeFalsy();
-    expect(isUrl('ttp://example.com/')).toBeFalsy();
-  });
-
-  it('should return true for valid URLs', () => {
-    expect(isUrl('http://example.com/')).toBeTruthy();
-    expect(isUrl('https://example.com/')).toBeTruthy();
-    expect(isUrl('http://example.com/test/123')).toBeTruthy();
-    expect(isUrl('https://example.com/test/123')).toBeTruthy();
-    expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('http://www.example.com/')).toBeTruthy();
-    expect(isUrl('https://www.example.com/')).toBeTruthy();
-    expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
-    expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
-    expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
-    expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
-  });
-});

src/utils/utils.js → src/utils/utils.ts


+ 3 - 2
tsconfig.json

@@ -9,18 +9,19 @@
     "jsx": "react",
     "allowSyntheticDefaultImports": true,
     "moduleResolution": "node",
-    "rootDirs": ["/src", "/test", "/mock","./typings"],
+    "rootDirs": ["/src", "/test", "/mock", "./typings"],
     "forceConsistentCasingInFileNames": true,
     "noImplicitReturns": true,
     "suppressImplicitAnyIndexErrors": true,
     "noUnusedLocals": true,
     "allowJs": true,
     "experimentalDecorators": true,
+    "strictNullChecks": true,
     "paths": {
       "@/*": ["./src/*"]
     }
   },
-  "include": ["./src"],
+  "include": ["./src", "config/defaultSettings.ts"],
   "exclude": [
     "node_modules",
     "build",

+ 3 - 1
tslint.json

@@ -6,6 +6,8 @@
     "object-literal-sort-keys": false,
     "jsx-no-lambda": false,
     "no-implicit-dependencies": false,
-    "no-console": false
+    "no-console": false,
+    "member-access": false,
+    "prefer-conditional-expression": false
   }
 }