Commit 0bc812b7 authored by 何乐's avatar 何乐 Committed by 陈小聪

[v4] Strict ts (#3723)

* short-term use of tslint

* fix lint error

* connect & dispatch definition

* replace `SFC` with `FunctionComponent`

* SiderMenu

* TopNavHeader

* HeaderSearch

* components

* layouts

* layouts

* pages

* fix authorize fail

* SettingDrawer

* remove one `as any`
parent 1abac6a3
......@@ -35,5 +35,7 @@ module.exports = {
},
settings: {
polyfills: ['fetch', 'promises', 'url'],
// support import modules from TypeScript files in JavaScript files
'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx'] } },
},
};
{
"singleQuote": true,
"trailingComma": "es5",
"trailingComma": "all",
"printWidth": 100,
"overrides": [
{
......
// https://umijs.org/config/
import os from 'os';
// import os from 'os';
import slash from 'slash2';
import { IPlugin } from 'umi-types';
import { IPlugin, IConfig } from 'umi-types';
import defaultSettings from './defaultSettings';
import webpackPlugin from './plugin.config';
......@@ -144,6 +144,5 @@ export default {
manifest: {
basePath: '/',
},
chainWebpack: webpackPlugin,
};
} as IConfig;
export declare type SiderTheme = 'light' | 'dark';
import { MenuTheme } from 'antd/es/menu';
export type ContentWidth = 'Fluid' | 'Fixed';
export interface DefaultSettings {
navTheme: string | SiderTheme;
/**
* theme for nav menu
*/
navTheme: MenuTheme;
/**
* primary color of ant design
*/
primaryColor: string;
layout: string;
contentWidth: string;
/**
* nav menu position: `sidemenu` or `topmenu`
*/
layout: 'sidemenu' | 'topmenu';
/**
* layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
*/
contentWidth: ContentWidth;
/**
* sticky header
*/
fixedHeader: boolean;
/**
* auto hide header
*/
autoHideHeader: boolean;
/**
* sticky siderbar
*/
fixSiderbar: boolean;
menu: { disableLocal: boolean };
title: string;
pwa: boolean;
/**
* your iconfont Symbol Scrip Url
* eg:`//at.alicdn.com/t/font_1039637_btcrd5co4w.js`
* 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
*/
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
contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu
fixedHeader: false, // sticky header
autoHideHeader: false, // auto hide header
fixSiderbar: false, // sticky siderbar
navTheme: 'dark',
primaryColor: '#1890FF',
layout: 'sidemenu',
contentWidth: 'Fluid',
fixedHeader: false,
autoHideHeader: false,
fixSiderbar: false,
colorWeak: false,
menu: {
disableLocal: false,
},
title: 'Ant Design Pro',
pwa: true,
// your iconfont Symbol Scrip Url
// eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
// 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
iconfontUrl: '',
} as DefaultSettings;
......@@ -11,12 +11,15 @@
"dev:no-mock": "cross-env MOCK=none umi dev",
"build": "umi build",
"analyze": "cross-env ANALYZE=1 umi build",
"lint:js": "eslint --ext .js src tests",
"lint:ts": "tslint -p . -c tslint.yml",
"lint:style": "stylelint 'src/**/*.less' --syntax less",
"lint:prettier": "check-prettier lint",
"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": "npm run lint:js && npm run lint:ts && npm run lint:style && npm run lint:prettier",
"lint:fix": "eslint --fix --ext .js src tests && tslint --fix -p . -c tslint.yml && stylelint --fix 'src/**/*.less' --syntax less",
"lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js",
"lint-staged:ts": "tslint",
"test": "umi test",
"test:component": "umi test ./src/components",
"test:all": "node ./tests/run-tests.js",
......@@ -46,14 +49,17 @@
"react-container-query": "^0.11.0",
"react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3",
"react-media": "^1.8.0",
"react-media-hook2": "^1.0.2",
"umi-request": "^1.0.0",
"umi-types": "^0.2.0"
"umi-request": "^1.0.0"
},
"devDependencies": {
"@types/classnames": "^2.2.7",
"@types/enzyme": "^3.9.0",
"@types/jest": "^24.0.11",
"@types/lodash": "^4.14.122",
"@types/memoize-one": "^4.1.0",
"@types/react": "^16.8.1",
"@types/react-document-title": "^2.0.3",
"@types/react-dom": "^16.0.11",
"antd-pro-merge-less": "^1.0.0",
"antd-theme-webpack-plugin": "^1.2.0",
......@@ -90,11 +96,13 @@
"stylelint-order": "^2.0.0",
"tslint": "^5.12.1",
"tslint-config-prettier": "^1.17.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-react": "^3.6.0",
"umi": "^2.4.4",
"umi-plugin-ga": "^1.1.3",
"umi-plugin-pro-block": "^1.2.0",
"umi-plugin-react": "^1.3.4"
"umi-plugin-react": "^1.3.4",
"umi-types": "^0.2.0"
},
"optionalDependencies": {
"puppeteer": "^1.12.1"
......@@ -105,6 +113,7 @@
"git add"
],
"**/*.{js,jsx}": "npm run lint-staged:js",
"**/*.{ts,tsx}": "npm run lint-staged:ts",
"**/*.less": "stylelint --syntax less"
},
"engines": {
......
import fetch from 'dva/fetch';
import { IRoute } from 'umi-types';
export const dva = {
config: {
onError(err) {
onError(err: ErrorEvent) {
err.preventDefault();
},
},
......@@ -10,7 +11,7 @@ export const dva = {
let authRoutes = {};
function ergodicRoutes(routes, authKey, authority) {
function ergodicRoutes(routes: IRoute[], authKey: string, authority: string | string[]) {
routes.forEach(element => {
if (element.path === authKey) {
if (!element.authority) element.authority = []; // eslint-disable-line
......@@ -22,14 +23,14 @@ function ergodicRoutes(routes, authKey, authority) {
});
}
export function patchRoutes(routes) {
export function patchRoutes(routes: IRoute[]) {
Object.keys(authRoutes).map(authKey =>
ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
ergodicRoutes(routes, authKey, authRoutes[authKey].authority),
);
(window as any).g_routes = routes;
}
export function render(oldRender) {
export function render(oldRender: Function) {
fetch('/api/auth_routes')
.then(res => res.json())
.then(
......@@ -39,6 +40,6 @@ export function render(oldRender) {
},
() => {
oldRender();
}
},
);
}
import { ConnectProps } from '@/models/connect';
import { NoticeItem } from '@/models/global';
import { CurrentUser } from '@/models/user';
import React, { Component } from 'react';
import { FormattedMessage, formatMessage } from 'umi-plugin-locale';
import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
......@@ -10,30 +13,20 @@ import HeaderDropdown from '../HeaderDropdown';
import SelectLang from '../SelectLang';
import styles from './index.less';
export declare type SiderTheme = 'light' | 'dark';
export 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;
};
export interface GlobalHeaderRightProps extends ConnectProps {
notices?: NoticeItem[];
currentUser?: CurrentUser;
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onMenuClick?: (param: ClickParam) => void;
onNoticeClear?: (tabName: string) => void;
theme?: SiderTheme;
}
export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
getNoticeData() {
getNoticeData = (): { [key: string]: NoticeItem[] } => {
const { notices = [] } = this.props;
if (notices.length === 0) {
return {};
......@@ -41,7 +34,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
const newNotices = notices.map(notice => {
const newNotice = { ...notice };
if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow();
newNotice.datetime = moment(notice.datetime as string).fromNow();
}
if (newNotice.id) {
newNotice.key = newNotice.id;
......@@ -62,10 +55,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
return newNotice;
});
return groupBy(newNotices, 'type');
}
};
getUnreadData: (noticeData: object) => any = noticeData => {
const unreadMsg = {};
getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
const unreadMsg: { [key: string]: number } = {};
Object.entries(noticeData).forEach(([key, value]) => {
if (!unreadMsg[key]) {
unreadMsg[key] = 0;
......@@ -77,10 +70,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
return unreadMsg;
};
changeReadState = clickedItem => {
changeReadState = (clickedItem: NoticeItem) => {
const { id } = clickedItem;
const { dispatch } = this.props;
dispatch({
dispatch!({
type: 'global/changeNoticeReadState',
payload: id,
});
......@@ -133,10 +126,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
formatMessage({ id: 'component.globalHeader.search.example3' }),
]}
onSearch={value => {
console.log('input', value); // eslint-disable-line
console.log('input', value); // tslint:disable-line no-console
}}
onPressEnter={value => {
console.log('enter', value); // eslint-disable-line
console.log('enter', value); // tslint:disable-line no-console
}}
/>
<Tooltip title={formatMessage({ id: 'component.globalHeader.help' })}>
......@@ -152,23 +145,23 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
<NoticeIcon
className={styles.action}
count={currentUser.unreadCount}
count={currentUser && currentUser.unreadCount}
onItemClick={(item, tabProps) => {
console.log(item, tabProps); // eslint-disable-line
this.changeReadState(item);
console.log(item, tabProps); // tslint:disable-line no-console
this.changeReadState(item as NoticeItem);
}}
loading={fetchingNotices}
locale={{
emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
clear: formatMessage({ id: 'component.noticeIcon.clear' }),
viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }), // todo:node_modules/ant-design-pro/lib/NoticeIcon/index.d.ts 21 [key: string]: string;
viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }),
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')} // todo:onViewMore?: (tabProps: INoticeIconProps) => void;
onViewMore={() => message.info('Click on view more')}
clearClose
>
<NoticeIcon.Tab
......@@ -196,7 +189,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
showViewMore
/>
</NoticeIcon>
{currentUser.name ? (
{currentUser && currentUser.name ? (
<HeaderDropdown overlay={menu}>
<span className={`${styles.action} ${styles.account}`}>
<Avatar
......
......@@ -3,31 +3,35 @@ import { Icon } from 'antd';
import Link from 'umi/link';
import debounce from 'lodash/debounce';
import styles from './index.less';
import RightContent from './RightContent';
import RightContent, { GlobalHeaderRightProps } from './RightContent';
interface GlobalHeaderProps {
type PartialGlobalHeaderRightProps = {
[K in
| 'onMenuClick'
| 'onNoticeClear'
| 'onNoticeVisibleChange'
| 'currentUser']?: GlobalHeaderRightProps[K]
};
export interface GlobalHeaderProps extends PartialGlobalHeaderRightProps {
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();
}
triggerResizeEvent = debounce(() => {
// eslint-disable-line
const event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, false);
window.dispatchEvent(event);
});
componentWillUnmount() {
this.triggerResizeEvent.cancel();
}
toggle = () => {
const { collapsed, onCollapse } = this.props;
onCollapse(!collapsed);
if (onCollapse) onCollapse(!collapsed);
this.triggerResizeEvent();
};
render() {
......
import React, { Component } from 'react';
import React from 'react';
import { Dropdown } from 'antd';
import { DropDownProps } from 'antd/es/dropdown';
import classNames from 'classnames';
import styles from './index.less';
declare type OverlayFunc = () => React.ReactNode;
interface HeaderDropdownProps {
export interface HeaderDropdownProps extends DropDownProps {
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;
const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
);
return (
<Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
);
}
}
export default HeaderDropdown;
import React, { Component } from 'react';
import { Input, Icon, AutoComplete } from 'antd';
import InputProps from 'antd/es/input';
import { DataSourceItemType } from 'antd/es/auto-complete';
import classNames from 'classnames';
import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind';
import styles from './index.less';
interface HeaderSearchProps {
export interface HeaderSearchProps {
onPressEnter: (value: string) => void;
onSearch: (value: string) => void;
onChange: (value: string) => void;
......@@ -15,7 +14,7 @@ interface HeaderSearchProps {
className: string;
placeholder: string;
defaultActiveFirstOption: boolean;
dataSource: any[];
dataSource: DataSourceItemType[];
defaultOpen: boolean;
open?: boolean;
}
......@@ -24,6 +23,7 @@ interface HeaderSearchState {
value: string;
searchMode: boolean;
}
export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
static defaultProps = {
defaultActiveFirstOption: false,
......@@ -37,7 +37,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
onVisibleChange: () => {},
};
static getDerivedStateFromProps(props) {
static getDerivedStateFromProps(props: HeaderSearchProps) {
if ('open' in props) {
return {
searchMode: props.open,
......@@ -46,9 +46,10 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
return null;
}
timeout: NodeJS.Timeout;
input: InputProps;
constructor(props) {
private timeout: NodeJS.Timeout = null!;
private inputRef: Input | null = null;
constructor(props: HeaderSearchProps) {
super(props);
this.state = {
searchMode: props.defaultOpen,
......@@ -60,7 +61,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
clearTimeout(this.timeout);
}
onKeyDown = e => {
onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
const { onPressEnter } = this.props;
const { value } = this.state;
......@@ -70,7 +71,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
}
};
onChange = value => {
onChange = (value: string) => {
const { onSearch, onChange } = this.props;
this.setState({ value });
if (onSearch) {
......@@ -86,8 +87,8 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
onVisibleChange(true);
this.setState({ searchMode: true }, () => {
const { searchMode } = this.state;
if (searchMode) {
this.input.focus();
if (searchMode && this.inputRef) {
this.inputRef.focus();
}
});
};
......@@ -135,11 +136,11 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
{...restProps}
className={inputClass}
value={value}
onChange={this.onChange}
onChange={this.onChange as any}
>
<Input
ref={node => {
this.input = node;
this.inputRef = node;
}}
aria-label={placeholder}
placeholder={placeholder}
......
......@@ -3,7 +3,7 @@ import { Spin } from 'antd';
// loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
const PageLoding: React.SFC = () => (
const PageLoding: React.FC = () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" />
</div>
......
import React from 'react';
import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale';
import { Menu, Icon } from 'antd';
import { ClickParam } from 'antd/es/menu';
import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
......@@ -8,12 +9,10 @@ import styles from './index.less';
interface SelectLangProps {
className?: string;
}
const SelectLang: React.SFC<SelectLangProps> = props => {
const SelectLang: React.FC<SelectLangProps> = props => {
const { className } = props;
const selectedLang = getLocale();
const changeLang = ({ key }) => {
setLocale(key);
};
const changeLang = ({ key }: ClickParam) => setLocale(key);
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
const languageLabels = {
'zh-CN': '简体中文',
......
......@@ -2,12 +2,13 @@ import React from 'react';
import { Tooltip, Icon } from 'antd';
import style from './index.less';
interface BlockChecboxProps {
export interface BlockChecboxProps {
value: string;
onChange: (key: string) => void;
list: any[];
}
const BlockChecbox: React.SFC<BlockChecboxProps> = ({ value, onChange, list }) => (
const BlockChecbox: React.FC<BlockChecboxProps> = ({ value, onChange, list }) => (
<div className={style.blockChecbox} key={value}>
{list.map(item => (
<Tooltip title={item.title} key={item.key}>
......
......@@ -3,34 +3,28 @@ import { Tooltip, Icon } from 'antd';
import { formatMessage } from 'umi-plugin-locale';
import styles from './ThemeColor.less';
interface TagProps {
export interface TagProps {
color: string;
check: boolean;
className?: string;
onClick?: () => void;
}
const Tag: React.SFC<TagProps> = ({ color, check, ...rest }) => (
<div
{...rest}
style={{
backgroundColor: color,
}}
>
const Tag: React.FC<TagProps> = ({ color, check, ...rest }) => (
<div {...rest} style={{ backgroundColor: color }}>
{check ? <Icon type="check" /> : ''}
</div>
);
interface ThemeColorProps {
export 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 = [
const ThemeColor: React.FC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
const colorList = colors || [
{
key: 'dust',
color: '#F5222D',
......@@ -64,7 +58,6 @@ const ThemeColor: React.SFC<ThemeColorProps> = ({ colors, title, value, onChange
color: '#722ED1',
},
];
}
return (
<div className={styles.themeColor}>
<h3 className={styles.title}>{title}</h3>
......
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import React, { Component } from 'react';
import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd';
import { formatMessage } from 'umi-plugin-locale';
......@@ -7,7 +8,6 @@ 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 {
......@@ -15,34 +15,37 @@ interface BodyProps {
style?: React.CSSProperties;
}
const Body: React.SFC<BodyProps> = ({ children, title, style }) => (
<div
style={{
...style,
marginBottom: 24,
}}
>
const Body: React.FC<BodyProps> = ({ children, title, style }) => (
<div style={{ ...style, marginBottom: 24 }}>
<h3 className={styles.title}>{title}</h3>
{children}
</div>
);
interface SettingDrawerProps {
setting?: DefaultSettings;
dispatch?: (args: any) => void;
interface SettingItemProps {
title: React.ReactNode;
action: React.ReactElement;
disabled?: boolean;
disabledReason?: React.ReactNode;
}
export interface SettingDrawerProps extends ConnectProps {
setting?: SettingModelState;
}
interface SettingDrawerState {}
@connect(({ setting }) => ({ setting }))
export interface SettingDrawerState extends Partial<SettingModelState> {
collapse: boolean;
}
@connect(({ setting }: ConnectState) => ({ setting }))
class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
state = {
state: SettingDrawerState = {
collapse: false,
};
getLayoutSetting = () => {
const {
setting: { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar },
} = this.props;
getLayoutSetting = (): SettingItemProps[] => {
const { setting } = this.props;
const { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar } = setting!;
return [
{
title: formatMessage({ id: 'app.setting.content-width' }),
......@@ -101,9 +104,9 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
];
};
changeSetting = (key, value) => {
changeSetting = (key: string, value: any) => {
const { setting } = this.props;
const nextState = { ...setting };
const nextState = { ...setting! };
nextState[key] = value;
if (key === 'layout') {
nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid';
......@@ -112,7 +115,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
}
this.setState(nextState, () => {
const { dispatch } = this.props;
dispatch({
dispatch!({
type: 'setting/changeSetting',
payload: this.state,
});
......@@ -124,7 +127,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
this.setState({ collapse: !collapse });
};
renderLayoutSettingItem = item => {
renderLayoutSettingItem = (item: SettingItemProps) => {
const action = React.cloneElement(item.action, {
disabled: item.disabled,
});
......@@ -139,7 +142,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
render() {
const { setting } = this.props;
const { navTheme, primaryColor, layout, colorWeak } = setting;
const { navTheme, primaryColor, layout, colorWeak } = setting!;
const { collapse } = this.state;
return (
<Drawer
......@@ -149,18 +152,10 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
placement="right"
handler={
<div className={styles.handle} onClick={this.togglerContent}>
<Icon
type={collapse ? 'close' : 'setting'}
style={{
color: '#fff',
fontSize: 20,
}}
/>
<Icon type={collapse ? 'close' : 'setting'} style={{ color: '#fff', fontSize: 20 }} />
</div>
}
style={{
zIndex: 999,
}}
style={{ zIndex: 999 }}
>
<div className={styles.content}>
<Body title={formatMessage({ id: 'app.setting.pagestyle' })}>
......@@ -221,6 +216,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
<List.Item
actions={[
<Switch
key="Switch"
size="small"
checked={!!colorWeak}
onChange={checked => this.changeSetting('colorWeak', checked)}
......
import IconFont from '@/components/IconFont';
import { isUrl } from '@/utils/utils';
import { Icon, Menu } from 'antd';
import { MenuMode, MenuTheme } from 'antd/es/menu';
import classNames from 'classnames';
import * as H from 'history';
import React, { Component } from 'react';
import Link from 'umi/link';
import { urlToList } from '../_utils/pathTools';
......@@ -16,7 +16,7 @@ const { SubMenu } = Menu;
// icon: 'icon-geren' #For Iconfont ,
// icon: 'http://demo.com/icon.png',
// icon: <Icon type="setting" />,
const getIcon = icon => {
const getIcon = (icon?: string | React.ReactNode) => {
if (typeof icon === 'string') {
if (isUrl(icon)) {
return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />;
......@@ -29,42 +29,55 @@ const getIcon = icon => {
return icon;
};
export declare type CollapseType = 'clickTrigger' | 'responsive';
export declare type SiderTheme = 'light' | 'dark';
export declare type MenuMode =
| 'vertical'
| 'vertical-left'
| 'vertical-right'
| 'horizontal'
| 'inline';
/**
* @type R: is route
*/
export interface MenuDataItem<R extends boolean = false> {
authority?: string[] | string;
children?: MenuDataItem[];
hideChildrenInMenu?: boolean;
hideInMenu?: boolean;
icon?: string;
locale?: string;
name?: string;
path: string;
routes?: R extends true ? MenuDataItem<R>[] : never;
[key: string]: any;
}
interface BaseMenuProps {
export interface BaseMenuProps {
className?: string;
collapsed?: boolean;
flatMenuKeys?: any[];
location?: H.Location;
onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
handleOpenChange?: (openKeys: string[]) => void;
isMobile?: boolean;
openKeys?: any;
theme?: SiderTheme;
location?: Location;
menuData?: MenuDataItem[];
mode?: MenuMode;
className?: string;
collapsed?: boolean;
handleOpenChange?: (openKeys: any[]) => void;
menuData?: any[];
style?: React.CSSProperties;
onCollapse?: (collapsed: boolean) => void;
onOpenChange?: (openKeys: string[]) => void;
openKeys?: string[];
style?: React.CSSProperties;
theme?: MenuTheme;
}
interface BaseMenuState {}
export default class BaseMenu extends Component<BaseMenuProps> {
static defaultProps: BaseMenuProps = {
flatMenuKeys: [],
location: window.location,
onCollapse: () => void 0,
isMobile: false,
openKeys: [],
collapsed: false,
handleOpenChange: () => void 0,
menuData: [],
onOpenChange: () => void 0,
};
export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
getNavMenuItems: (menusData: any[]) => any[] = menusData => {
if (!menusData) {
return [];
}
getNavMenuItems = (menusData: MenuDataItem[] = []): React.ReactNode[] => {
return menusData
.filter(item => item.name && !item.hideInMenu)
.map(item => this.getSubMenuOrItem(item))
......@@ -72,18 +85,22 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
};
// Get the currently selected menu
getSelectedMenuKeys = pathname => {
getSelectedMenuKeys = (pathname: string): string[] => {
const { flatMenuKeys } = this.props;
return urlToList(pathname).map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop());
return urlToList(pathname)
.map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop())
.filter(item => item) as string[];
};
/**
* get SubMenu or Item
*/
getSubMenuOrItem = item => {
// doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
const { name } = item;
getSubMenuOrItem = (item: MenuDataItem): React.ReactNode => {
if (
Array.isArray(item.children) &&
!item.hideChildrenInMenu &&
item.children.some(child => (child.name ? true : false))
) {
return (
<SubMenu
title={
......@@ -93,7 +110,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
<span>{name}</span>
</span>
) : (
name
item.name
)
}
key={item.path}
......@@ -110,7 +127,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
* Judge whether it is http link.return a or Link
* @memberof SiderMenu
*/
getMenuItemPath = item => {
getMenuItemPath = (item: MenuDataItem) => {
const { name } = item;
const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon);
......@@ -129,14 +146,8 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
<Link
to={itemPath}
target={target}
replace={itemPath === location.pathname}
onClick={
isMobile
? () => {
onCollapse(true);
}
: undefined
}
replace={itemPath === location!.pathname}
onClick={isMobile ? () => onCollapse!(true) : void 0}
>
{icon}
<span>{name}</span>
......@@ -144,7 +155,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
);
};
conversionPath = path => {
conversionPath = (path: string) => {
if (path && path.indexOf('http') === 0) {
return path;
}
......@@ -156,7 +167,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
openKeys,
theme,
mode,
location: { pathname },
location,
className,
collapsed,
handleOpenChange,
......@@ -164,7 +175,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
menuData,
} = this.props;
// if pathname can't match, use the nearest parent's key
let selectedKeys = this.getSelectedMenuKeys(pathname);
let selectedKeys = this.getSelectedMenuKeys(location!.pathname);
if (!selectedKeys.length && openKeys) {
selectedKeys = [openKeys[openKeys.length - 1]];
}
......
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 defaultSettings from '../../../config/defaultSettings';
import PageLoading from '../PageLoading';
import { BaseMenuProps } from './BaseMenu';
import styles from './index.less';
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
......@@ -13,38 +13,42 @@ const { Sider } = Layout;
const { title } = defaultSettings;
let firstMount: boolean = true;
export declare type CollapseType = 'clickTrigger' | 'responsive';
export declare type SiderTheme = 'light' | 'dark';
interface SiderMenuProps {
menuData: any[];
location?: H.Location;
flatMenuKeys?: any[];
export interface SiderMenuProps extends BaseMenuProps {
logo?: string;
collapsed: boolean;
onCollapse: (collapsed: boolean, type?: CollapseType) => void;
fixSiderbar?: boolean;
theme?: SiderTheme;
isMobile: boolean;
}
interface SiderMenuState {
openKeys: any;
pathname?: string;
openKeys?: string[];
flatMenuKeysLen?: number;
}
export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> {
static getDerivedStateFromProps(props, state) {
static defaultProps: SiderMenuProps = {
flatMenuKeys: [],
location: window.location,
onCollapse: () => void 0,
isMobile: false,
openKeys: [],
collapsed: false,
handleOpenChange: () => void 0,
menuData: [],
onOpenChange: () => void 0,
};
static getDerivedStateFromProps(props: SiderMenuProps, state: SiderMenuState) {
const { pathname, flatMenuKeysLen } = state;
if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
if (props.location!.pathname !== pathname || props.flatMenuKeys!.length !== flatMenuKeysLen) {
return {
pathname: props.location.pathname,
flatMenuKeysLen: props.flatMenuKeys.length,
pathname: props.location!.pathname,
flatMenuKeysLen: props.flatMenuKeys!.length,
openKeys: getDefaultCollapsedSubMenus(props),
};
}
return null;
}
constructor(props: SiderMenuProps) {
super(props);
this.state = {
......@@ -58,7 +62,7 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
isMainMenu: (key: string) => boolean = key => {
const { menuData } = this.props;
return menuData.some(item => {
return menuData!.some(item => {
if (key) {
return item.key === key || item.path === key;
}
......@@ -66,11 +70,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
});
};
handleOpenChange: (openKeys: any[]) => void = openKeys => {
handleOpenChange: (openKeys: string[]) => void = openKeys => {
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
this.setState({
openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
});
if (moreThanOne) {
this.setState({ openKeys: [openKeys.pop()].filter(item => item) as string[] });
} else {
this.setState({ openKeys: [...openKeys] });
}
};
render() {
......@@ -84,13 +90,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
});
return (
<Sider
collapsible
trigger={null}
collapsible={true}
collapsed={collapsed}
breakpoint="lg"
onCollapse={collapse => {
if (firstMount || !isMobile) {
onCollapse(collapse);
onCollapse!(collapse);
}
}}
width={256}
......
import pathToRegexp from 'path-to-regexp';
import { urlToList } from '../_utils/pathTools';
import { MenuDataItem, BaseMenuProps } from './BaseMenu';
/**
* Recursively flatten the data
* [{path:string},{path:string}] => {path,path2}
* @param menus
*/
export const getFlatMenuKeys = menuData => {
let keys = [];
export const getFlatMenuKeys = (menuData: MenuDataItem[] = []) => {
let keys: string[] = [];
menuData.forEach(item => {
keys.push(item.path);
if (item.children) {
......@@ -17,24 +18,15 @@ export const getFlatMenuKeys = menuData => {
return keys;
};
export const getMenuMatches = (flatMenuKeys, path) =>
flatMenuKeys.filter(item => {
if (item) {
return pathToRegexp(item).test(path);
}
return false;
});
export const getMenuMatches = (flatMenuKeys: string[] = [], path: string) =>
flatMenuKeys.filter(item => item && pathToRegexp(item).test(path));
/**
* 获得菜单子节点
* @memberof SiderMenu
*/
export const getDefaultCollapsedSubMenus = props => {
const {
location: { pathname },
flatMenuKeys,
} = props;
return urlToList(pathname)
export const getDefaultCollapsedSubMenus = (props: BaseMenuProps) => {
const { location, flatMenuKeys } = props;
return urlToList(location!.pathname)
.map(item => getMenuMatches(flatMenuKeys, item)[0])
.filter(item => item)
.reduce((acc, curr) => [...acc, curr], ['/']);
......
import React from 'react';
import { Drawer } from 'antd';
import { SiderMenuProps } from './SiderMenu';
import SiderMenu from './SiderMenu';
import { getFlatMenuKeys } from './SiderMenuUtils';
export declare type SiderTheme = 'light' | 'dark';
export { SiderMenuProps };
export { MenuDataItem } from './BaseMenu';
interface SiderMenuProps {
isMobile: boolean;
menuData: any[];
collapsed: boolean;
logo?: string;
theme?: SiderTheme;
onCollapse: (payload: boolean) => void;
}
const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
const SiderMenuWrapper: React.FC<SiderMenuProps> = props => {
const { isMobile, menuData, collapsed, onCollapse } = props;
const flatMenuKeys = getFlatMenuKeys(menuData);
return isMobile ? (
<Drawer
visible={!collapsed}
placement="left"
onClose={() => onCollapse(true)}
onClose={() => onCollapse!(true)}
style={{
padding: 0,
height: '100vh',
......@@ -34,4 +27,8 @@ const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
);
};
SiderMenuWrapper.defaultProps = {
onCollapse: () => void 0,
};
export default React.memo(SiderMenuWrapper);
import { SiderMenuProps } from '@/components/SiderMenu';
import React, { Component } from 'react';
import Link from 'umi/link';
import RightContent from '../GlobalHeader/RightContent';
import RightContent, { GlobalHeaderRightProps } from '../GlobalHeader/RightContent';
import BaseMenu from '../SiderMenu/BaseMenu';
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
import styles from './index.less';
import defaultSettings from '../../../config/defaultSettings';
import defaultSettings, { ContentWidth } from '../../../config/defaultSettings';
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;
export interface TopNavHeaderProps extends SiderMenuProps, GlobalHeaderRightProps {
contentWidth?: ContentWidth;
}
interface TopNavHeaderState {
maxWidth: undefined | number;
maxWidth?: number;
}
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
state = {
maxWidth: undefined,
};
maim: HTMLDivElement;
static getDerivedStateFromProps(props) {
static getDerivedStateFromProps(props: TopNavHeaderProps) {
return {
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40,
};
}
state: TopNavHeaderState = {};
maim: HTMLDivElement | null = null;
render() {
const { theme, contentWidth, menuData, logo } = this.props;
const { maxWidth } = this.state;
......@@ -60,23 +33,17 @@ export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHea
return (
<div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
<div
ref={ref => {
this.maim = ref;
}}
ref={ref => (this.maim = ref)}
className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`}
>
<div className={styles.left}>
<div className={styles.logo} key="logo" id="logo">
<Link to="/">
<img src={logo} alt="logo" />
<h1>{title}</h1>
<h1>{defaultSettings.title}</h1>
</Link>
</div>
<div
style={{
maxWidth,
}}
>
<div style={{ maxWidth }}>
<BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} className={styles.menu} />
</div>
</div>
......
import 'jest';
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',
]);
});
});
// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
// eslint-disable-next-line import/prefer-default-export
export function urlToList(url) {
export function urlToList(url: string) {
const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
}
......@@ -14,7 +14,8 @@ 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: CustomEvent) => {
window.addEventListener('sw.updated', (event: Event) => {
const e = event as 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
......@@ -25,11 +26,11 @@ if (pwa) {
// Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => {
const channel = new MessageChannel();
channel.port1.onmessage = event => {
if (event.data.error) {
reject(event.data.error);
channel.port1.onmessage = msgEvent => {
if (msgEvent.data.error) {
reject(msgEvent.data.error);
} else {
resolve(event.data);
resolve(msgEvent.data);
}
};
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
......
import PageLoading from '@/components/PageLoading';
import SiderMenu from '@/components/SiderMenu';
import SiderMenu, { MenuDataItem, SiderMenuProps } from '@/components/SiderMenu';
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import getPageTitle from '@/utils/getPageTitle';
import { Layout } from 'antd';
import classNames from 'classnames';
......@@ -11,7 +12,7 @@ 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 Header, { HeaderViewProps } from './Header';
import Context from './MenuContext';
// lazy load SettingDrawer
......@@ -44,28 +45,21 @@ const query = {
},
};
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;
export interface BasicLayoutProps
extends ConnectProps,
SiderMenuProps,
HeaderViewProps,
Partial<SettingModelState> {
breadcrumbNameMap: { [path: string]: MenuDataItem };
route: MenuDataItem;
}
interface BasicLayoutContext {
export interface BasicLayoutContext {
location: Location;
breadcrumbNameMap: object;
breadcrumbNameMap: { [path: string]: MenuDataItem };
}
const BasicLayout: React.SFC<BasicLayoutProps> = props => {
const BasicLayout: React.FC<BasicLayoutProps> = props => {
const {
breadcrumbNameMap,
dispatch,
......@@ -79,18 +73,21 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
navTheme,
route: { routes, authority },
} = props;
/**
* constructor
*/
useState(() => {
dispatch({ type: 'user/fetchCurrent' });
dispatch({ type: 'setting/getSetting' });
dispatch({ type: 'menu/getMenuData', payload: { routes, authority } });
dispatch!({ type: 'user/fetchCurrent' });
dispatch!({ type: 'setting/getSetting' });
dispatch!({ type: 'menu/getMenuData', payload: { routes, authority } });
});
const isTop = PropsLayout === 'topmenu';
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {};
/**
* init variables
*/
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 });
dispatch!({ type: 'global/changeLayoutCollapsed', payload });
// Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo
const renderSettingDrawer = () =>
......@@ -98,7 +95,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
const layout = (
<Layout>
{isTop && !isMobile ? null : (
{PropsLayout === 'topmenu' && !isMobile ? null : (
<SiderMenu
logo={logo}
theme={navTheme}
......@@ -121,7 +118,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
isMobile={isMobile}
{...props}
/>
<Content className={styles.content} style={contentStyle}>
<Content className={styles.content} style={!fixedHeader ? { paddingTop: 0 } : {}}>
{children}
</Content>
<Footer />
......@@ -130,10 +127,10 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
);
return (
<React.Fragment>
<DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}>
<DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
<ContainerQuery query={query}>
{params => (
<Context.Provider value={getContext()}>
<Context.Provider value={{ location: location!, breadcrumbNameMap }}>
<div className={classNames(params)}>{layout}</div>
</Context.Provider>
)}
......@@ -144,7 +141,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
);
};
export default connect(({ global, setting, menu: menuModel }) => ({
export default connect(({ global, setting, menu: menuModel }: ConnectState) => ({
collapsed: global.collapsed,
layout: setting.layout,
menuData: menuModel.menuData,
......
import React, { ReactNode, SFC } from 'react';
import React from 'react';
interface BlankLayoutProps {
children: ReactNode;
}
const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
const Layout: React.FC = ({ children }) => <div>{children}</div>;
export default Layout;
import GlobalHeader from '@/components/GlobalHeader';
import TopNavHeader from '@/components/TopNavHeader';
import { DefaultSettings } from '../../config/defaultSettings';
import GlobalHeader, { GlobalHeaderProps } from '@/components/GlobalHeader';
import TopNavHeader, { TopNavHeaderProps } from '@/components/TopNavHeader';
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import { Layout, message } from 'antd';
import { ClickParam } from 'antd/es/menu';
import { connect } from 'dva';
import Animate from 'rc-animate';
import React, { Component } from 'react';
......@@ -11,15 +12,12 @@ import styles from './Header.less';
const { Header } = Layout;
export declare type SiderTheme = 'light' | 'dark';
interface HeaderViewProps {
isMobile: boolean;
collapsed: boolean;
setting: DefaultSettings;
dispatch: (args: any) => void;
autoHideHeader: boolean;
handleMenuCollapse: (args: boolean) => void;
export interface HeaderViewProps extends ConnectProps, TopNavHeaderProps, GlobalHeaderProps {
isMobile?: boolean;
collapsed?: boolean;
setting?: SettingModelState;
autoHideHeader?: boolean;
handleMenuCollapse?: (collapse: boolean) => void;
}
interface HeaderViewState {
......@@ -27,6 +25,10 @@ interface HeaderViewState {
}
class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
static defaultProps: HeaderViewProps = {
handleMenuCollapse: () => void 0,
};
static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) {
if (!props.autoHideHeader && !state.visible) {
return {
......@@ -36,14 +38,12 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
return null;
}
state = {
ticking: boolean = false;
oldScrollTop: number = 0;
state: HeaderViewState = {
visible: true,
};
ticking: boolean;
oldScrollTop: number;
componentDidMount() {
document.addEventListener('scroll', this.handScroll, { passive: true });
}
......@@ -54,27 +54,27 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
getHeadWidth = () => {
const { isMobile, collapsed, setting } = this.props;
const { fixedHeader, layout } = setting;
const { fixedHeader, layout } = setting!;
if (isMobile || !fixedHeader || layout === 'topmenu') {
return '100%';
}
return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)';
};
handleNoticeClear = type => {
handleNoticeClear = (type: string) => {
const { dispatch } = this.props;
message.success(
`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({
id: `component.globalHeader.${type}`,
})}`
})}`,
);
const { dispatch } = this.props;
dispatch({
dispatch!({
type: 'global/clearNotices',
payload: type,
});
};
handleMenuClick = ({ key }) => {
handleMenuClick = ({ key }: ClickParam) => {
const { dispatch } = this.props;
if (key === 'userCenter') {
router.push('/account/center');
......@@ -89,16 +89,16 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
return;
}
if (key === 'logout') {
dispatch({
dispatch!({
type: 'login/logout',
});
}
};
handleNoticeVisibleChange = visible => {
handleNoticeVisibleChange = (visible: boolean) => {
if (visible) {
const { dispatch } = this.props;
dispatch({
dispatch!({
type: 'global/fetchNotices',
});
}
......@@ -135,7 +135,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
render() {
const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting;
const { navTheme, layout, fixedHeader } = setting!;
const { visible } = this.state;
const isTop = layout === 'topmenu';
const width = this.getHeadWidth();
......@@ -143,7 +143,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
<Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}>
{isTop && !isMobile ? (
<TopNavHeader
theme={navTheme as SiderTheme}
theme={navTheme}
mode="horizontal"
onCollapse={handleMenuCollapse}
onNoticeClear={this.handleNoticeClear}
......@@ -170,7 +170,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
}
}
export default connect(({ user, global, setting, loading }) => ({
export default connect(({ user, global, setting, loading }: ConnectState) => ({
currentUser: user.currentUser,
collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
......
import SelectLang from '@/components/SelectLang';
import { MenuDataItem } from '@/components/SiderMenu';
import { ConnectProps, ConnectState } from '@/models/connect';
import getPageTitle from '@/utils/getPageTitle';
import { GlobalFooter } from 'ant-design-pro';
import { Icon } from 'antd';
......@@ -34,12 +36,10 @@ const copyright = (
</Fragment>
);
interface UserLayoutProps {
dispatch: (args: any) => void;
route: any;
breadcrumbNameMap: object;
export interface UserLayoutProps extends ConnectProps {
route: MenuDataItem;
breadcrumbNameMap: { [path: string]: MenuDataItem };
navTheme: string;
location: Location;
}
class UserLayout extends Component<UserLayoutProps> {
......@@ -48,20 +48,16 @@ class UserLayout extends Component<UserLayoutProps> {
dispatch,
route: { routes, authority },
} = this.props;
dispatch({
dispatch!({
type: 'menu/getMenuData',
payload: { routes, authority },
});
}
render() {
const {
children,
location: { pathname },
breadcrumbNameMap,
} = this.props;
const { children, location, breadcrumbNameMap } = this.props;
return (
<DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}>
<DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
<div className={styles.container}>
<div className={styles.lang}>
<SelectLang />
......@@ -85,7 +81,7 @@ class UserLayout extends Component<UserLayoutProps> {
}
}
export default connect(({ menu: menuModel }) => ({
export default connect(({ menu: menuModel }: ConnectState) => ({
menuData: menuModel.menuData,
breadcrumbNameMap: menuModel.breadcrumbNameMap,
}))(UserLayout);
import { EffectsCommandMap } from 'dva';
import { AnyAction } from 'redux';
import { GlobalModelState } from './global';
import { MenuModelState } from './menu';
import { UserModelState } from './user';
import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
export { GlobalModelState, MenuModelState, SettingModelState, UserModelState };
export type Effect = (
action: AnyAction,
effects: EffectsCommandMap & { select: <T>(func: (state: ConnectState) => T) => T },
) => void;
/**
* @type P: Type of payload
* @type C: Type of callback
*/
export type Dispatch = <P = any, C = (payload: P) => void>(action: {
type: string;
payload?: P;
callback?: C;
[key: string]: any;
}) => any;
export interface Loading {
global: boolean;
effects: { [key: string]: boolean | undefined };
models: {
global?: boolean;
menu?: boolean;
setting?: boolean;
user?: boolean;
};
}
export interface ConnectState {
global: GlobalModelState;
loading: Loading;
menu: MenuModelState;
setting: SettingModelState;
user: UserModelState;
}
/**
* @type T: Params matched in dynamic routing
* @type R: Instance type of ref
*/
export interface ConnectProps<T extends { [key: string]: any } = {}, R = any>
extends React.Props<R> {
dispatch?: Dispatch;
location?: Location;
match?: {
isExact: boolean;
params: T;
path: string;
url: string;
};
}
export default ConnectState;
import { queryNotices } from '@/services/user';
import { Effect, Subscription } from 'dva';
import { Subscription } from 'dva';
import { Reducer } from 'redux';
import { Effect } from './connect';
import { INoticeIconData } from 'ant-design-pro/lib/NoticeIcon/NoticeIconTab';
export interface NoticeItem extends INoticeIconData {
id: string;
type: string;
[key: string]: any;
}
export interface GlobalModelState {
collapsed: boolean;
notices: any[];
notices: NoticeItem[];
}
export interface GlobalModelType {
......@@ -16,9 +24,9 @@ export interface GlobalModelType {
changeNoticeReadState: Effect;
};
reducers: {
changeLayoutCollapsed: Reducer<any>;
saveNotices: Reducer<any>;
saveClearedNotices: Reducer<any>;
changeLayoutCollapsed: Reducer<GlobalModelState>;
saveNotices: Reducer<GlobalModelState>;
saveClearedNotices: Reducer<GlobalModelState>;
};
subscriptions: { setup: Subscription };
}
......@@ -38,8 +46,8 @@ const GlobalModel: GlobalModelType = {
type: 'saveNotices',
payload: data,
});
const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length
const unreadCount: number = yield select(
state => state.global.notices.filter(item => !item.read).length,
);
yield put({
type: 'user/changeNotifyCount',
......@@ -54,9 +62,9 @@ const GlobalModel: GlobalModelType = {
type: 'saveClearedNotices',
payload,
});
const count = yield select(state => state.global.notices.length);
const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length
const count: number = yield select(state => state.global.notices.length);
const unreadCount: number = yield select(
state => state.global.notices.filter(item => !item.read).length,
);
yield put({
type: 'user/changeNotifyCount',
......@@ -67,14 +75,14 @@ const GlobalModel: GlobalModelType = {
});
},
*changeNoticeReadState({ payload }, { put, select }) {
const notices = yield select(state =>
const notices: NoticeItem[] = yield select(state =>
state.global.notices.map(item => {
const notice = { ...item };
if (notice.id === payload) {
notice.read = true;
}
return notice;
})
}),
);
yield put({
type: 'saveNotices',
......
import { MenuDataItem } from '@/components/SiderMenu';
import Authorized from '@/utils/Authorized';
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 { IRoute } from 'umi-types';
import defaultSettings from '../../config/defaultSettings';
const { menu } = defaultSettings;
const { check } = Authorized;
// Conversion router to menu.
function formatter(data: any[], parentAuthority: string[], parentName: string): any[] {
function formatter(
data: MenuDataItem[],
parentAuthority?: string[] | string,
parentName?: string,
): MenuDataItem[] {
return data
.filter(item => item.name && item.path)
.map(item => {
if (!item.name || !item.path) {
return null;
}
let locale = 'menu';
if (parentName) {
locale = `${parentName}.${item.name}`;
} else {
locale = `menu.${item.name}`;
}
const locale = `${parentName || 'menu'}.${item.name!}`;
// if enableMenuLocale use item.name,
// close menu international
const name = menu.disableLocal
? item.name
: formatMessage({ id: locale, defaultMessage: item.name });
const result = {
const name = defaultSettings.menu.disableLocal
? item.name!
: formatMessage({ id: locale, defaultMessage: item.name! });
const result: MenuDataItem = {
...item,
name,
locale,
......@@ -40,55 +36,43 @@ function formatter(data: any[], parentAuthority: string[], parentName: string):
}
delete result.routes;
return result;
})
.filter(item => item);
});
}
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: SubMenuItem) => any = item => {
// doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
return {
...item,
children: filterMenuData(item.children), // eslint-disable-line
};
const getSubMenu: (item: MenuDataItem) => MenuDataItem = item => {
if (
Array.isArray(item.children) &&
!item.hideChildrenInMenu &&
item.children.some(child => (child.name ? true : false))
) {
const children = filterMenuData(item.children);
if (children.length) return { ...item, children };
}
return item;
return { ...item, children: void 0 };
};
/**
* filter menuData
*/
const filterMenuData: (menuData: SubMenuItem[]) => SubMenuItem[] = menuData => {
if (!menuData) {
return [];
}
const filterMenuData = (menuData: MenuDataItem[] = []): MenuDataItem[] => {
return menuData
.filter(item => item.name && !item.hideInMenu)
.map(item => check(item.authority, getSubMenu(item), null))
.map(item => Authorized.check<any, any>(item.authority!, getSubMenu(item), null))
.filter(item => item);
};
/**
* 获取面包屑映射
* @param ISubMenuItem[] menuData 菜单配置
* @param MenuDataItem[] menuData 菜单配置
*/
const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
const routerMap = {};
const flattenMenuData: (data: SubMenuItem[]) => void = data => {
const getBreadcrumbNameMap = (menuData: MenuDataItem[]) => {
const routerMap: { [key: string]: MenuDataItem } = {};
const flattenMenuData: (data: MenuDataItem[]) => void = data => {
data.forEach(menuItem => {
if (menuItem.children) {
flattenMenuData(menuItem.children);
......@@ -104,8 +88,8 @@ const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
export interface MenuModelState {
menuData: any[];
routerData: any[];
menuData: MenuDataItem[];
routerData: IRoute[];
breadcrumbNameMap: object;
}
......@@ -116,9 +100,10 @@ export interface MenuModelType {
getMenuData: Effect;
};
reducers: {
save: Reducer<any>;
save: Reducer<MenuModelState>;
};
}
const MenuModel: MenuModelType = {
namespace: 'menu',
......
......@@ -24,8 +24,8 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => {
const hideMessage = message.loading('正在编译主题!', 0);
function buildIt() {
if (!(window as any).less) {
console.log('no less');
return;
// tslint:disable-next-line no-console
return console.log('no less');
}
setTimeout(() => {
(window as any).less
......
......@@ -2,9 +2,7 @@ import { query as queryUsers, queryCurrent } from '@/services/user';
import { Effect } from 'dva';
import { Reducer } from 'redux';
export interface UserModelState {
list: any[];
currentUser: {
export interface CurrentUser {
avatar?: string;
name?: string;
title?: string;
......@@ -13,7 +11,11 @@ export interface UserModelState {
geographic?: any;
tags?: any[];
unreadCount?: number;
};
}
export interface UserModelState {
list: any[];
currentUser: CurrentUser;
}
export interface UserModelType {
......
import Authorized from '@/utils/Authorized';
import { ConnectProps, ConnectState, UserModelState } from '@/models/connect';
import { connect } from 'dva';
import pathToRegexp from 'path-to-regexp';
import React from 'react';
import Redirect from 'umi/redirect';
import { UserModelState } from '../models/user';
import { IRoute } from 'umi-types';
interface AuthComponentProps {
interface AuthComponentProps extends ConnectProps {
location: Location;
routerData: any[];
routerData: IRoute[];
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;
const getRouteAuthority = (path: string, routeData: IRoute[]) => {
let authorities: string[] | string | undefined = void 0;
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;
......@@ -28,10 +25,14 @@ const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, rout
}
});
return authorities;
};
};
const AuthComponent: React.FC<AuthComponentProps> = ({ children, location, routerData, user }) => {
const { currentUser } = user;
const isLogin = currentUser && currentUser.name;
return (
<Authorized
authority={getRouteAuthority(location.pathname, routerData)}
authority={getRouteAuthority(location.pathname, routerData)!}
noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
>
{children}
......@@ -39,7 +40,7 @@ const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, rout
);
};
export default connect(({ menu: menuModel, user }) => ({
export default connect(({ menu: menuModel, user }: ConnectState) => ({
routerData: menuModel.routerData,
user,
}))(AuthComponent);
......@@ -38,11 +38,11 @@ workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
*/
workbox.routing.registerRoute(
/^https:\/\/gw.alipayobjects.com\//,
workbox.strategies.networkFirst()
workbox.strategies.networkFirst(),
);
workbox.routing.registerRoute(
/^https:\/\/cdnjs.cloudflare.com\//,
workbox.strategies.networkFirst()
workbox.strategies.networkFirst(),
);
workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
......@@ -58,8 +58,8 @@ addEventListener('message', event => {
.skipWaiting()
.then(
() => replyPort.postMessage({ error: null }),
error => replyPort.postMessage({ error })
)
error => replyPort.postMessage({ error }),
),
);
}
});
......@@ -9,4 +9,33 @@ declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'rc-animate';
declare module 'omit.js';
declare module 'react-copy-to-clipboard';
declare var APP_TYPE: string;
declare module 'ant-design-pro' {
import React from 'react';
import { INoticeIconProps } from 'ant-design-pro/lib/NoticeIcon';
import { INoticeIconTabProps } from 'ant-design-pro/lib/NoticeIcon/NoticeIconTab';
type PartialNoticeIconProps = {
[K in Exclude<keyof INoticeIconProps, 'locale'>]?: INoticeIconProps[K]
};
interface MixinNoticeIconProps extends PartialNoticeIconProps {
locale?: {
emptyText: string;
clear: string;
viewMore: string;
[key: string]: string;
};
onViewMore?: (tabProps: INoticeIconProps) => void;
}
interface MixinNoticeIconTabProps extends Partial<INoticeIconTabProps> {
showViewMore?: boolean;
}
class NoticeIconTab extends React.Component<MixinNoticeIconTabProps, any> {}
export class NoticeIcon extends React.Component<MixinNoticeIconProps, any> {
public static Tab: typeof NoticeIconTab;
}
export * from 'ant-design-pro/lib';
}
......@@ -3,7 +3,7 @@ import { getAuthority } from './authority';
describe('getAuthority should be strong', () => {
it('empty', () => {
expect(getAuthority(null)).toEqual(['admin']); // default value
expect(getAuthority(null!)).toEqual(['admin']); // default value
});
it('string', () => {
expect(getAuthority('admin')).toEqual(['admin']);
......
......@@ -6,7 +6,7 @@ export function getAuthority(str?: string): any {
// authorityString could be admin, "admin", ["admin"]
let authority;
try {
authority = JSON.parse(authorityString);
authority = JSON.parse(authorityString!);
} catch (e) {
authority = authorityString;
}
......
......@@ -3,24 +3,22 @@ import memoizeOne from 'memoize-one';
import pathToRegexp from 'path-to-regexp';
import { formatMessage } from 'umi-plugin-locale';
import defaultSettings from '../../config/defaultSettings';
import { MenuDataItem } from '@/components/SiderMenu/BaseMenu';
const { menu, title } = defaultSettings;
interface RouterData {
name: string;
locale: string;
authority?: string[];
children?: any[];
icon?: string;
path: string;
}
export const matchParamsPath = (pathname: string, breadcrumbNameMap: object): RouterData => {
export const matchParamsPath = (
pathname: string,
breadcrumbNameMap: { [path: string]: MenuDataItem },
): MenuDataItem => {
const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname));
return breadcrumbNameMap[pathKey];
return breadcrumbNameMap[pathKey!];
};
const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
const getPageTitle = (
pathname: string,
breadcrumbNameMap: { [path: string]: MenuDataItem },
): string => {
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap);
if (!currRouterData) {
return title;
......@@ -28,7 +26,7 @@ const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
const pageName = menu.disableLocal
? currRouterData.name
: formatMessage({
id: currRouterData.locale || currRouterData.name,
id: currRouterData.locale || currRouterData.name!,
defaultMessage: currRouterData.name,
});
......
......@@ -5,6 +5,12 @@
import { extend } from 'umi-request';
import { notification } from 'antd';
interface ResponseError<D = any> extends Error {
name: string;
data: D;
response: Response;
}
const codeMessage = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
......@@ -26,8 +32,8 @@ const codeMessage = {
/**
* 异常处理程序
*/
const errorHandler = error => {
const { response = {} } = error;
const errorHandler = (error: ResponseError) => {
const { response = {} as Response } = error;
const errortext = codeMessage[response.status] || response.statusText;
const { status, url } = response;
......
import 'jest';
import { isUrl } from './utils';
describe('isUrl tests', () => {
it('should return false for invalid and corner case inputs', () => {
expect(isUrl([] as any)).toBeFalsy();
expect(isUrl({} as any)).toBeFalsy();
expect(isUrl(false as any)).toBeFalsy();
expect(isUrl(true as any)).toBeFalsy();
expect(isUrl(NaN as any)).toBeFalsy();
expect(isUrl(null as any)).toBeFalsy();
expect(isUrl(void 0 as any)).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();
});
});
/* eslint no-useless-escape:0 import/prefer-default-export:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
export function isUrl(path) {
export function isUrl(path: string) {
return reg.test(path);
}
......@@ -2,8 +2,8 @@
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es2016",
"lib": ["es6", "dom"],
"target": "esnext",
"lib": ["esnext", "dom"],
"sourceMap": true,
"baseUrl": ".",
"jsx": "react",
......@@ -16,7 +16,7 @@
"noUnusedLocals": true,
"allowJs": true,
"experimentalDecorators": true,
"strictNullChecks": true,
"strict": true,
"paths": {
"@/*": ["./src/*"]
}
......
{
"extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
"rules": {
"no-var-requires": false,
"no-submodule-imports": false,
"object-literal-sort-keys": false,
"jsx-no-lambda": false,
"no-implicit-dependencies": false,
"no-console": false,
"member-access": false,
"prefer-conditional-expression": false
}
}
defaultSeverity: error
extends:
- tslint-react
- tslint-eslint-rules
- tslint-config-prettier
jsRules:
rules:
class-name: true
eofline: true
forin: true
jsdoc-format: false
label-position: true
member-ordering:
- true
- order: statics-first
new-parens: true
no-arg: true
no-bitwise: true
no-conditional-assignment: true
no-consecutive-blank-lines: true
no-console:
- true
- debug
- info
- log
- time
- timeEnd
- trace
- warn
no-construct: true
no-debugger: true
no-duplicate-variable: true
no-eval: true
no-internal-module: true
no-multi-spaces: true
no-namespace: true
no-reference: true
no-shadowed-variable: true
no-string-literal: true
no-trailing-whitespace: true
no-unused-expression: true
no-var-keyword: true
one-variable-per-declaration:
- true
- ignore-for-loop
prefer-const:
- true
- destructuring: all
radix: true
space-in-parens: true
switch-default: true
trailing-comma:
- true
- singleline: never
multiline: always
esSpecCompliant: true
triple-equals:
- true
- allow-null-check
typedef-whitespace:
- true
- call-signature: nospace
index-signature: nospace
parameter: nospace
property-declaration: nospace
variable-declaration: nospace
- call-signature: onespace
index-signature: onespace
parameter: onespace
property-declaration: onespace
variable-declaration: onespace
use-isnan: true
variable-name:
- true
- allow-leading-underscore
- ban-keywords
- check-format
- allow-pascal-case
jsx-no-lambda: false
jsx-no-string-ref: false
jsx-boolean-value:
- true
- never
jsx-no-multiline-js: false
whitespace:
- true
- check-branch
- check-decl
- check-operator
- check-module
- check-separator
- check-rest-spread
- check-type
- check-type-operator
- check-preblock
declare module 'slash2';
declare module 'antd-pro-merge-less';
declare module 'antd-theme-webpack-plugin';
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