Unverified Commit 1abac6a3 authored by 陈小聪's avatar 陈小聪 Committed by GitHub

[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
parent e0b20cb7
// 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',
{
......
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;
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));
});
});
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');
});
});
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();
});
});
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));
});
});
......@@ -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"
......
......@@ -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) {
......
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}
......
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);
......
import * as React from 'react';
export default class HeaderDropdown extends React.Component<any, any> {}
import React, { PureComponent } from 'react';
import React, { Component } from 'react';
import { Dropdown } from 'antd';
import classNames from 'classnames';
import styles from './index.less';
export default class HeaderDropdown extends PureComponent {
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} />
);
......
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> {}
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 = {
......
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' />
......
......@@ -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;
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>
);
}
}
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;
......@@ -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}>
......
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 = [
......
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>
);
......
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',
});
......
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',
]);
});
});
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 => {
......
......@@ -24,6 +24,7 @@ export const getMenuMatches = (flatMenuKeys, path) =>
}
return false;
});
/**
* 获得菜单子节点
* @memberof SiderMenu
......
......@@ -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);
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,
......
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',
]);
});
});
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
......
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>
));
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);
import React from 'react';
export default ({ children }) => <div>{children}</div>;
import React, { ReactNode, SFC } from 'react';
interface BlankLayoutProps {
children: ReactNode;
}
const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
export default Layout;
import { Icon, Layout } from 'antd';
import React, { Fragment } from 'react';
import { Layout, Icon } from 'antd';
import { GlobalFooter } from 'ant-design-pro';
const { Footer } = Layout;
......
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}
......
import { createContext } from 'react';
export default createContext();
export default createContext({});
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,
......
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;
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;
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;
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;
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 pathToRegexp from 'path-to-regexp';
import { connect } from 'dva';
import Authorized from '@/utils/Authorized';
import { UserModelState } from '../models/user';
function AuthComponent({ children, location, routerData, currentCuser }) {
const isLogin = currentCuser && currentCuser.name;
interface AuthComponentProps {
location: Location;
routerData: any[];
user: UserModelState;
}
const getRouteAuthority = (pathname, routeData) => {
const routes = routeData.slice(); // clone
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;
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);
}
// get children authority recursively
if (route.routes) {
authorities = getRouteAuthority(path, route.routes) || authorities;
}
});
return authorities;
};
return getAuthority(routes, pathname);
}
});
return authorities;
};
return (
<Authorized
......@@ -37,8 +37,9 @@ function AuthComponent({ children, location, routerData, currentCuser }) {
{children}
</Authorized>
);
}
export default connect(({ menu: menuModel, user: userModel }) => ({
};
export default connect(({ menu: menuModel, user }) => ({
routerData: menuModel.routerData,
currentCuser: userModel.currentCuser,
user,
}))(AuthComponent);
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');
}
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;
import 'jest';
import { getAuthority } from './authority';
describe('getAuthority should be strong', () => {
......
// 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));
}
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;
......
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();
});
});
......@@ -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",
......
......@@ -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
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment