diff --git a/.eslintrc.js b/.eslintrc.js index 7372d96b..ee148d56 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,17 @@ module.exports = { - parser: 'babel-eslint', - extends: ['airbnb', 'prettier', 'plugin:compat/recommended'], + extends: [ + 'airbnb', + 'prettier', + 'plugin:compat/recommended', + 'airbnb-typescript', + 'plugin:@typescript-eslint/recommended', + 'plugin:eslint-comments/recommended', + 'plugin:jest/recommended', + 'plugin:promise/recommended', + 'prettier', + 'prettier/react', + 'prettier/@typescript-eslint', + ], env: { browser: true, node: true, @@ -10,8 +21,8 @@ module.exports = { jasmine: true, }, globals: { - page: true, ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, + page: true, }, rules: { 'react/jsx-filename-extension': [1, { extensions: ['.js'] }], @@ -27,16 +38,39 @@ module.exports = { devDependencies: ['**/tests/**.js', '/mock/**/**.js', '**/**.test.js'], }, ], - 'import/no-cycle': 0, 'jsx-a11y/no-noninteractive-element-interactions': 0, 'jsx-a11y/click-events-have-key-events': 0, 'jsx-a11y/no-static-element-interactions': 0, 'jsx-a11y/anchor-is-valid': 0, 'linebreak-style': 0, + // Too restrictive, writing ugly code to defend against a very unlikely scenario: https://eslint.org/docs/rules/no-prototype-builtins + 'no-prototype-builtins': 'off', + 'import/prefer-default-export': 'off', + 'import/no-default-export': [true, 'camel-case'], + // Too restrictive: https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/destructuring-assignment.md + 'react/destructuring-assignment': 'off', + // No jsx extension: https://github.com/facebook/create-react-app/issues/87#issuecomment-234627904 + 'react/jsx-filename-extension': 'off', + // Use function hoisting to improve code readability + 'no-use-before-define': ['error', { functions: false, classes: true, variables: true }], + // Makes no sense to allow type inferrence for expression parameters, but require typing the response + '@typescript-eslint/explicit-function-return-type': [ + 'off', + { allowTypedFunctionExpressions: true }, + ], + '@typescript-eslint/no-use-before-define': [ + 'error', + { functions: false, classes: true, variables: true, typedefs: true }, + ], + // Common abbreviations are known and readable + 'unicorn/prevent-abbreviations': 'off', + '@typescript-eslint/explicit-member-accessibility': 0, + 'import/no-cycle': 0, }, + plugins: ['@typescript-eslint', 'eslint-comments', 'jest', 'promise', 'unicorn'], settings: { // support import modules from TypeScript files in JavaScript files 'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx'] } }, - polyfills: ['fetch', 'promises', 'url', 'object-assign'], + polyfills: ['fetch', 'Promise', 'URL', 'object-assign'], }, }; diff --git a/.gitignore b/.gitignore index 67e95cb3..18b814ab 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ functions/* # screenshot screenshot -.firebase \ No newline at end of file +.firebase +.eslintcache diff --git a/package.json b/package.json index 96c560d1..bfa8d068 100644 --- a/package.json +++ b/package.json @@ -19,21 +19,18 @@ "generateMock": "node ./scripts/generateMock", "lint": "npm run lint:js && npm run lint:ts && npm run lint:style && npm run lint:prettier", "lint-staged": "lint-staged", - "lint-staged:js": "eslint --ext .js", - "lint-staged:ts": "tslint", - "lint:fix": "eslint --fix --ext .js src tests && npm run lint:style && npm run tslint:fix", - "lint:js": "eslint --ext .js src tests", + "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src && npm run lint:style && npm run tslint:fix", + "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", "lint:prettier": "check-prettier lint", "lint:style": "stylelint --fix \"src/**/*.less\" --syntax less", - "lint:ts": "tslint -p . -c tslint.yml", "prettier": " check-prettier write", "site": "npm run fetch:blocks && npm run functions:build && umi build", "start": "umi dev", "start:no-mock": "cross-env MOCK=none umi dev", "test": "umi test", "test:all": "node ./tests/run-tests.js", - "test:component": "umi test ./src/components", - "tslint:fix": "tslint --fix \"src/**/*.ts*\"" + "test:component": "umi test ./src/components" }, "husky": { "hooks": { @@ -42,12 +39,11 @@ }, "lint-staged": { "**/*.less": "stylelint --syntax less", - "**/*.{js,jsx}": "npm run lint-staged:js", + "**/*.{js,ts,tsx}": "npm run lint-staged:js", "**/*.{js,ts,tsx,md,json,jsx,less}": [ "npm run prettier", "git add" - ], - "**/*.{ts,tsx}": "npm run lint-staged:ts" + ] }, "browserslist": [ "> 1%", @@ -76,6 +72,7 @@ "react-dom": "^16.8.6", "react-media": "^1.9.2", "react-media-hook2": "^1.0.5", + "redux": "^4.0.1", "umi": "^2.7.0-beta.2", "umi-plugin-ga": "^1.1.3", "umi-plugin-locale": "^2.8.0-beta.1", @@ -92,6 +89,7 @@ "@types/react": "^16.8.19", "@types/react-document-title": "^2.0.3", "@types/react-dom": "^16.8.4", + "@typescript-eslint/eslint-plugin": "^1.9.0", "antd-pro-merge-less": "^1.0.0", "antd-theme-webpack-plugin": "^1.2.0", "babel-eslint": "^10.0.1", @@ -102,16 +100,26 @@ "enzyme": "^3.9.0", "eslint": "^5.16.0", "eslint-config-airbnb": "^17.1.0", - "eslint-config-prettier": "^4.3.0", + "eslint-config-airbnb-typescript": "^4.0.0", + "eslint-config-prettier": "^4.1.0", + "eslint-formatter-pretty": "^2.1.1", "eslint-plugin-babel": "^5.3.0", "eslint-plugin-compat": "^3.1.1", + "eslint-plugin-eslint-comments": "^3.1.1", "eslint-plugin-import": "^2.17.3", - "eslint-plugin-jsx-a11y": "^6.2.1", + "eslint-plugin-jest": "^22.4.1", + "eslint-plugin-jsx-a11y": "^6.2.0", "eslint-plugin-markdown": "^1.0.0", - "eslint-plugin-react": "^7.13.0", + "eslint-plugin-promise": "^4.1.1", + "eslint-plugin-react": "^7.12.4", + "eslint-plugin-unicorn": "^8.0.1", "express": "^4.17.1", "gh-pages": "^2.0.1", "husky": "^2.3.0", + "import-sort-cli": "^6.0.0", + "import-sort-parser-babylon": "^6.0.0", + "import-sort-parser-typescript": "^6.0.0", + "import-sort-style-module": "^6.0.0", "jest-puppeteer": "^4.2.0", "jsdom-global": "^3.0.2", "less": "^3.9.0", @@ -128,11 +136,7 @@ "stylelint-config-rational-order": "^0.1.2", "stylelint-config-standard": "^18.3.0", "stylelint-declaration-block-no-ignored-properties": "^2.1.0", - "stylelint-order": "^3.0.0", - "tslint": "^5.17.0", - "tslint-config-prettier": "^1.18.0", - "tslint-eslint-rules": "^5.4.0", - "tslint-react": "^4.0.0" + "stylelint-order": "^3.0.0" }, "optionalDependencies": { "puppeteer": "^1.17.0" @@ -175,4 +179,4 @@ "create-umi" ] } -} +} \ No newline at end of file diff --git a/src/components/Authorized/Authorized.tsx b/src/components/Authorized/Authorized.tsx index a8bbb5e0..b6bad65f 100644 --- a/src/components/Authorized/Authorized.tsx +++ b/src/components/Authorized/Authorized.tsx @@ -1,28 +1,26 @@ -import CheckPermissions from './CheckPermissions'; -import { IAuthorityType } from './CheckPermissions'; +import React from 'react'; import Secured from './Secured'; -import check from './CheckPermissions'; +import check, { IAuthorityType } from './CheckPermissions'; import AuthorizedRoute from './AuthorizedRoute'; -import React from 'react'; -interface IAuthorizedProps { +interface AuthorizedProps { authority: IAuthorityType; noMatch?: React.ReactNode; } -type IAuthorizedType = React.FunctionComponent & { +type IAuthorizedType = React.FunctionComponent & { Secured: typeof Secured; check: typeof check; AuthorizedRoute: typeof AuthorizedRoute; }; -const Authorized: React.FunctionComponent = ({ +const Authorized: React.FunctionComponent = ({ children, authority, noMatch = null, }) => { const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children; - const dom = CheckPermissions(authority, childrenRender, noMatch); + const dom = check(authority, childrenRender, noMatch); return <>{dom}; }; diff --git a/src/components/Authorized/AuthorizedRoute.tsx b/src/components/Authorized/AuthorizedRoute.tsx index 7ff18c55..6c7deff8 100644 --- a/src/components/Authorized/AuthorizedRoute.tsx +++ b/src/components/Authorized/AuthorizedRoute.tsx @@ -3,7 +3,7 @@ import { Route, Redirect } from 'umi'; import Authorized from './Authorized'; import { IAuthorityType } from './CheckPermissions'; -interface IAuthorizedRoutePops { +interface AuthorizedRoutePops { currentAuthority: string; component: React.ComponentClass; render: (props: any) => React.ReactNode; @@ -11,7 +11,7 @@ interface IAuthorizedRoutePops { authority: IAuthorityType; } -const AuthorizedRoute: React.SFC = ({ +const AuthorizedRoute: React.SFC = ({ component: Component, render, authority, diff --git a/src/components/Authorized/PromiseRender.tsx b/src/components/Authorized/PromiseRender.tsx index 41db7fa0..0bcade9a 100644 --- a/src/components/Authorized/PromiseRender.tsx +++ b/src/components/Authorized/PromiseRender.tsx @@ -4,21 +4,21 @@ import React from 'react'; // eslint-disable-next-line import/no-cycle import { isComponentClass } from './Secured'; -interface IPromiseRenderProps { +interface PromiseRenderProps { ok: T; error: K; promise: Promise; } -interface IPromiseRenderState { +interface PromiseRenderState { component: React.ComponentClass | React.FunctionComponent; } export default class PromiseRender extends React.Component< - IPromiseRenderProps, - IPromiseRenderState + PromiseRenderProps, + PromiseRenderState > { - state: IPromiseRenderState = { + state: PromiseRenderState = { component: () => null, }; @@ -26,10 +26,7 @@ export default class PromiseRender extends React.Component< this.setRenderComponent(this.props); } - shouldComponentUpdate = ( - nextProps: IPromiseRenderProps, - nextState: IPromiseRenderState, - ) => { + shouldComponentUpdate = (nextProps: PromiseRenderProps, nextState: PromiseRenderState) => { const { component } = this.state; if (!isEqual(nextProps, this.props)) { this.setRenderComponent(nextProps); @@ -39,7 +36,7 @@ export default class PromiseRender extends React.Component< }; // set render Component : ok or error - setRenderComponent(props: IPromiseRenderProps) { + setRenderComponent(props: PromiseRenderProps) { const ok = this.checkIsInstantiation(props.ok); const error = this.checkIsInstantiation(props.error); props.promise @@ -47,6 +44,7 @@ export default class PromiseRender extends React.Component< this.setState({ component: ok, }); + return true; }) .catch(() => { this.setState({ diff --git a/src/components/Authorized/renderAuthorize.ts b/src/components/Authorized/renderAuthorize.ts index af7586cc..df008750 100644 --- a/src/components/Authorized/renderAuthorize.ts +++ b/src/components/Authorized/renderAuthorize.ts @@ -1,4 +1,7 @@ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable import/no-mutable-exports */ let CURRENT: string | string[] = 'NULL'; + type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); /** * use authority or getAuthority @@ -6,7 +9,7 @@ type CurrentAuthorityType = string | string[] | (() => typeof CURRENT); */ const renderAuthorize = (Authorized: T): ((currentAuthority: CurrentAuthorityType) => T) => ( currentAuthority: CurrentAuthorityType, -) => { +): T => { if (currentAuthority) { if (typeof currentAuthority === 'function') { CURRENT = currentAuthority(); diff --git a/src/components/CopyBlock/index.tsx b/src/components/CopyBlock/index.tsx index 0f7e6b92..c7e454f2 100644 --- a/src/components/CopyBlock/index.tsx +++ b/src/components/CopyBlock/index.tsx @@ -1,16 +1,15 @@ import React from 'react'; import { Icon, Typography, Popover } from 'antd'; -import styles from './index.less'; import { connect } from 'dva'; -import * as H from 'history'; import { FormattedMessage } from 'umi-plugin-react/locale'; +import styles from './index.less'; -const firstUpperCase = (pathString: string) => { +const firstUpperCase = (pathString: string): string => { return pathString .replace('.', '') - .split(/\/|\-/) - .map(s => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) - .filter(s => s) + .split(/\/|-/) + .map((s): string => s.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())) + .filter((s): boolean => !!s) .join(''); }; const BlockCodeView: React.SFC<{ @@ -26,7 +25,11 @@ const BlockCodeView: React.SFC<{ ); }; -type RoutingType = { location: H.Location }; +interface RoutingType { + location: { + pathname: string; + }; +} export default connect(({ routing }: { routing: RoutingType }) => ({ location: routing.location, diff --git a/src/components/GlobalFooter/index.tsx b/src/components/GlobalFooter/index.tsx index 01166243..7c492a9e 100644 --- a/src/components/GlobalFooter/index.tsx +++ b/src/components/GlobalFooter/index.tsx @@ -3,12 +3,12 @@ import classNames from 'classnames'; import styles from './index.less'; export interface GlobalFooterProps { - links?: Array<{ + links?: { key?: string; title: React.ReactNode; href: string; blankTarget?: boolean; - }>; + }[]; copyright?: React.ReactNode; style?: React.CSSProperties; className?: string; diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx index 5b135c6c..d96acaef 100644 --- a/src/components/GlobalHeader/AvatarDropdown.tsx +++ b/src/components/GlobalHeader/AvatarDropdown.tsx @@ -30,7 +30,8 @@ class AvatarDropdown extends React.Component { } router.push(`/account/${key}`); }; - render() { + + render(): React.ReactNode { const { currentUser = {}, menu } = this.props; if (!menu) { return ( diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx index d79550b0..ed5a8d7d 100644 --- a/src/components/GlobalHeader/NoticeIconView.tsx +++ b/src/components/GlobalHeader/NoticeIconView.tsx @@ -19,6 +19,37 @@ export interface GlobalHeaderRightProps extends ConnectProps { } class GlobalHeaderRight extends Component { + componentDidMount() { + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'global/fetchNotices', + }); + } + } + + changeReadState = (clickedItem: NoticeItem): void => { + const { id } = clickedItem; + const { dispatch } = this.props; + if (dispatch) { + dispatch({ + type: 'global/changeNoticeReadState', + payload: id, + }); + } + }; + + handleNoticeClear = (title: string, key: string) => { + const { dispatch } = this.props; + message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); + if (dispatch) { + dispatch({ + type: 'global/clearNotices', + payload: key, + }); + } + }; + getNoticeData = (): { [key: string]: NoticeItem[] } => { const { notices = [] } = this.props; if (notices.length === 0) { @@ -52,7 +83,8 @@ class GlobalHeaderRight extends Component { getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => { const unreadMsg: { [key: string]: number } = {}; - Object.entries(noticeData).forEach(([key, value]) => { + Object.keys(noticeData).forEach(key => { + const value = noticeData[key]; if (!unreadMsg[key]) { unreadMsg[key] = 0; } @@ -63,34 +95,6 @@ class GlobalHeaderRight extends Component { return unreadMsg; }; - changeReadState = (clickedItem: NoticeItem) => { - const { id } = clickedItem; - const { dispatch } = this.props; - if (dispatch) { - dispatch({ - type: 'global/changeNoticeReadState', - payload: id, - }); - } - }; - componentDidMount() { - const { dispatch } = this.props; - if (dispatch) { - dispatch({ - type: 'global/fetchNotices', - }); - } - } - handleNoticeClear = (title: string, key: string) => { - const { dispatch } = this.props; - message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); - if (dispatch) { - dispatch({ - type: 'global/clearNotices', - payload: key, - }); - } - }; render() { const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; const noticeData = this.getNoticeData(); diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx index cf4f5bf6..8d6695ff 100644 --- a/src/components/GlobalHeader/RightContent.tsx +++ b/src/components/GlobalHeader/RightContent.tsx @@ -1,5 +1,5 @@ import { ConnectProps, ConnectState } from '@/models/connect'; -import React, { Component } from 'react'; +import React from 'react'; import { Icon, Tooltip } from 'antd'; import { formatMessage } from 'umi-plugin-react/locale'; import HeaderSearch from '../HeaderSearch'; @@ -14,60 +14,58 @@ export interface GlobalHeaderRightProps extends ConnectProps { layout: 'sidemenu' | 'topmenu'; } -class GlobalHeaderRight extends Component { - render() { - const { theme, layout } = this.props; - let className = styles.right; +const GlobalHeaderRight: React.SFC = props => { + const { theme, layout } = props; + let className = styles.right; - if (theme === 'dark' && layout === 'topmenu') { - className = `${styles.right} ${styles.dark}`; - } + if (theme === 'dark' && layout === 'topmenu') { + className = `${styles.right} ${styles.dark}`; + } - return ( -
- { - console.log('input', value); // tslint:disable-line no-console - }} - onPressEnter={value => { - console.log('enter', value); // tslint:disable-line no-console - }} - /> - + { + console.log('input', value); + }} + onPressEnter={value => { + console.log('enter', value); + }} + /> + + - - - - - - -
- ); - } -} + + + + + + + ); +}; export default connect(({ settings }: ConnectState) => ({ theme: settings.navTheme, diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx index dc252603..08ed3ef2 100644 --- a/src/components/HeaderSearch/index.tsx +++ b/src/components/HeaderSearch/index.tsx @@ -45,7 +45,8 @@ export default class HeaderSearch extends Component { + this.timeout = window.setTimeout(() => { onPressEnter(value); // Fix duplicate onPressEnter }, 0); } diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx index 4ead5bf6..d44d3d3f 100644 --- a/src/components/NoticeIcon/index.tsx +++ b/src/components/NoticeIcon/index.tsx @@ -40,11 +40,11 @@ export default class NoticeIcon extends Component { public static Tab: typeof NoticeList = NoticeList; static defaultProps = { - onItemClick: () => {}, - onPopupVisibleChange: () => {}, - onTabChange: () => {}, - onClear: () => {}, - onViewMore: () => {}, + onItemClick: (): void => {}, + onPopupVisibleChange: (): void => {}, + onTabChange: (): void => {}, + onClear: (): void => {}, + onViewMore: (): void => {}, loading: false, clearClose: false, emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', @@ -54,64 +54,67 @@ export default class NoticeIcon extends Component { visible: false, }; - onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps) => { + onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => { const { onItemClick } = this.props; if (onItemClick) { onItemClick(item, tabProps); } }; - onClear = (name: string, key: string) => { + onClear = (name: string, key: string): void => { const { onClear } = this.props; if (onClear) { onClear(name, key); } }; - onTabChange = (tabType: string) => { + onTabChange = (tabType: string): void => { const { onTabChange } = this.props; if (onTabChange) { onTabChange(tabType); } }; - onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent) => { + onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => { const { onViewMore } = this.props; if (onViewMore) { onViewMore(tabProps, event); } }; - getNotificationBox() { + getNotificationBox(): React.ReactNode { const { children, loading, clearText, viewMoreText } = this.props; if (!children) { return null; } - const panes = React.Children.map(children, (child: React.ReactElement) => { - if (!child) { - return null; - } - const { list, title, count, tabKey, showClear, showViewMore } = child.props; - const len = list && list.length ? list.length : 0; - const msgCount = count || count === 0 ? count : len; - const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; - return ( - - this.onClear(title, tabKey)} - onClick={item => this.onItemClick(item, child.props)} - onViewMore={event => this.onViewMore(child.props, event)} - showClear={showClear} - showViewMore={showViewMore} - title={title} - {...child.props} - /> - - ); - }); + const panes = React.Children.map( + children, + (child: React.ReactElement): React.ReactNode => { + if (!child) { + return null; + } + const { list, title, count, tabKey, showClear, showViewMore } = child.props; + const len = list && list.length ? list.length : 0; + const msgCount = count || count === 0 ? count : len; + const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; + return ( + + this.onClear(title, tabKey)} + onClick={(item): void => this.onItemClick(item, child.props)} + onViewMore={(event): void => this.onViewMore(child.props, event)} + showClear={showClear} + showViewMore={showViewMore} + title={title} + {...child.props} + /> + + ); + }, + ); return ( @@ -121,7 +124,7 @@ export default class NoticeIcon extends Component { ); } - handleVisibleChange = (visible: boolean) => { + handleVisibleChange = (visible: boolean): void => { const { onPopupVisibleChange } = this.props; this.setState({ visible }); if (onPopupVisibleChange) { @@ -129,7 +132,7 @@ export default class NoticeIcon extends Component { } }; - render() { + render(): React.ReactNode { const { className, count, popupVisible, bell } = this.props; const { visible } = this.state; const noticeButtonClass = classNames(className, styles.noticeButton); diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx index ac8309d9..0f7810a3 100644 --- a/src/components/SelectLang/index.tsx +++ b/src/components/SelectLang/index.tsx @@ -12,7 +12,7 @@ interface SelectLangProps { const SelectLang: React.FC = props => { const { className } = props; const selectedLang = getLocale(); - const changeLang = ({ key }: ClickParam) => setLocale(key, false); + const changeLang = ({ key }: ClickParam): void => setLocale(key, false); const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; const languageLabels = { 'zh-CN': '简体中文', diff --git a/src/global.tsx b/src/global.tsx index 3a978862..caa1d494 100644 --- a/src/global.tsx +++ b/src/global.tsx @@ -19,7 +19,7 @@ if (pwa) { // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration const worker = e.detail && e.detail.waiting; if (!worker) { - return Promise.resolve(); + return true; } // Send skip-waiting event to waiting SW with MessageChannel await new Promise((resolve, reject) => { @@ -59,7 +59,12 @@ if (pwa) { }); } else if ('serviceWorker' in navigator) { // eslint-disable-next-line compat/compat - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); + navigator.serviceWorker.ready + .then(registration => { + registration.unregister(); + return true; + }) + .catch(() => { + console.log('serviceWorker unregister error'); + }); } diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 03d86f36..04d5c31f 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -18,6 +18,7 @@ import { Settings, } from '@ant-design/pro-layout'; import Link from 'umi/link'; + export interface BasicLayoutProps extends ProLayoutComponentsProps, ConnectProps { breadcrumbNameMap: { [path: string]: MenuDataItem; @@ -60,7 +61,7 @@ const BasicLayout: React.FC = props => { /** * init variables */ - const handleMenuCollapse = (payload: boolean) => + const handleMenuCollapse = (payload: boolean): void => dispatch && dispatch({ type: 'global/changeLayoutCollapsed', diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx index df0890aa..260f7fa2 100644 --- a/src/layouts/UserLayout.tsx +++ b/src/layouts/UserLayout.tsx @@ -2,7 +2,7 @@ import SelectLang from '@/components/SelectLang'; import GlobalFooter from '@/components/GlobalFooter'; import { ConnectProps } from '@/models/connect'; import { Icon } from 'antd'; -import React, { Component, Fragment } from 'react'; +import React, { Fragment } from 'react'; import DocumentTitle from 'react-document-title'; import { formatMessage } from 'umi-plugin-locale'; import Link from 'umi/link'; @@ -36,48 +36,48 @@ const copyright = ( export interface UserLayoutProps extends ConnectProps { breadcrumbNameMap: { [path: string]: MenuDataItem }; - navTheme: string; + navTheme: 'dark' | 'light'; } -class UserLayout extends Component { - render() { - const { - route = { - routes: [], - }, - } = this.props; - const { routes = [] } = route; - const { children, location } = this.props; - const { breadcrumb } = getMenuData(routes, this.props); - return ( - -
-
- -
-
-
-
- - logo - Ant Design - -
-
Ant Design 是西湖区最具影响力的 Web 设计规范
+const UserLayout: React.SFC = props => { + const { + route = { + routes: [], + }, + children, + location = { + pathname: '', + }, + } = props; + const { routes = [] } = route; + const { breadcrumb } = getMenuData(routes, props); + return ( + +
+
+ +
+
+
+
+ + logo + Ant Design +
- {children} +
Ant Design 是西湖区最具影响力的 Web 设计规范
- + {children}
- - ); - } -} - + +
+
+ ); +}; export default UserLayout; diff --git a/src/models/connect.d.ts b/src/models/connect.d.ts index 07b427a5..439dd3a8 100644 --- a/src/models/connect.d.ts +++ b/src/models/connect.d.ts @@ -1,27 +1,12 @@ import { EffectsCommandMap } from 'dva'; import { AnyAction } from 'redux'; import { RouterTypes } from 'umi'; +import { MenuDataItem } from '@ant-design/pro-layout'; import { GlobalModelState } from './global'; import { UserModelState } from './user'; import { DefaultSettings as SettingModelState } from '../../config/defaultSettings'; -import { MenuDataItem } from '@ant-design/pro-layout'; -export { GlobalModelState, SettingModelState, UserModelState }; -export type Effect = ( - action: AnyAction, - effects: EffectsCommandMap & { select: (func: (state: ConnectState) => T) => T }, -) => void; - -/** - * @type P: Type of payload - * @type C: Type of callback - */ -export type Dispatch =

void>(action: { - type: string; - payload?: P; - callback?: C; - [key: string]: any; -}) => any; +export { GlobalModelState, SettingModelState, UserModelState }; export interface Loading { global: boolean; @@ -41,6 +26,22 @@ export interface ConnectState { user: UserModelState; } +export type Effect = ( + action: AnyAction, + effects: EffectsCommandMap & { select: (func: (state: ConnectState) => T) => T }, +) => void; + +/** + * @type P: Type of payload + * @type C: Type of callback + */ +export type Dispatch =

void>(action: { + type: string; + payload?: P; + callback?: C; + [key: string]: any; +}) => any; + export interface Route extends MenuDataItem { routes?: Route[]; } @@ -52,5 +53,3 @@ export interface ConnectProps extends Partial> { dispatch?: Dispatch; } - -export default ConnectState; diff --git a/src/models/global.ts b/src/models/global.ts index 24a32d10..cb846904 100644 --- a/src/models/global.ts +++ b/src/models/global.ts @@ -1,8 +1,8 @@ import { queryNotices } from '@/services/user'; import { Subscription } from 'dva'; import { Reducer } from 'redux'; -import { Effect } from './connect'; import { NoticeIconData } from '@/components/NoticeIcon'; +import { Effect } from './connect.d'; export interface NoticeItem extends NoticeIconData { id: string; @@ -99,36 +99,38 @@ const GlobalModel: GlobalModelType = { }, reducers: { - changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }) { + changeLayoutCollapsed(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { return { ...state, collapsed: payload, }; }, - saveNotices(state, { payload }) { + saveNotices(state, { payload }): GlobalModelState { return { collapsed: false, ...state, notices: payload, }; }, - saveClearedNotices(state = { notices: [], collapsed: true }, { payload }) { + saveClearedNotices(state = { notices: [], collapsed: true }, { payload }): GlobalModelState { return { collapsed: false, ...state, - notices: state.notices.filter(item => item.type !== payload), + notices: state.notices.filter((item): boolean => item.type !== payload), }; }, }, subscriptions: { - setup({ history }) { + setup({ history }): void { // Subscribe history(url) change, trigger `load` action if pathname is `/` - return history.listen(({ pathname, search }) => { - if (typeof (window as any).ga !== 'undefined') { - (window as any).ga('send', 'pageview', pathname + search); - } - }); + history.listen( + ({ pathname, search }): void => { + if (typeof (window as any).ga !== 'undefined') { + (window as any).ga('send', 'pageview', pathname + search); + } + }, + ); }, }, }; diff --git a/src/models/login.ts b/src/models/login.ts index 18dfb971..d7e90ba0 100644 --- a/src/models/login.ts +++ b/src/models/login.ts @@ -3,7 +3,7 @@ import { Reducer, AnyAction } from 'redux'; import { EffectsCommandMap } from 'dva'; import { stringify, parse } from 'qs'; -export function getPageQuery() { +export function getPageQuery(): string { return parse(window.location.href.split('?')[1]); } diff --git a/src/models/setting.ts b/src/models/setting.ts index a5a278ae..6143167f 100644 --- a/src/models/setting.ts +++ b/src/models/setting.ts @@ -26,8 +26,8 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => { const hideMessage = message.loading('正在编译主题!', 0); function buildIt() { if (!(window as any).less) { - // tslint:disable-next-line no-console - return console.log('no less'); + console.log('no less'); + return; } setTimeout(() => { (window as any).less @@ -36,6 +36,7 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => { }) .then(() => { hideMessage(); + return true; }) .catch(() => { message.error('Failed to update theme'); diff --git a/src/pages/Authorized.tsx b/src/pages/Authorized.tsx index 6f502d36..5ec6fbbf 100644 --- a/src/pages/Authorized.tsx +++ b/src/pages/Authorized.tsx @@ -10,7 +10,7 @@ interface AuthComponentProps extends ConnectProps { } const getRouteAuthority = (path: string, routeData: Route[]) => { - let authorities: string[] | string | undefined = undefined; + let authorities: string[] | string | undefined; routeData.forEach(route => { // match prefix if (pathToRegexp(`${route.path}(.*)`).test(path)) { @@ -29,7 +29,9 @@ const AuthComponent: React.FC = ({ route = { routes: [], }, - location, + location = { + pathname: '', + }, user, }) => { const { currentUser } = user; @@ -37,7 +39,7 @@ const AuthComponent: React.FC = ({ const isLogin = currentUser && currentUser.name; return ( : } > {children} diff --git a/src/pages/Welcome.tsx b/src/pages/Welcome.tsx index cad44f13..4a471ac5 100644 --- a/src/pages/Welcome.tsx +++ b/src/pages/Welcome.tsx @@ -1,6 +1,6 @@ import React from 'react'; -export default () => ( +export default (): React.ReactNode => (

Want to add more pages? Please refer to{' '} diff --git a/src/service-worker.js b/src/service-worker.js index 3ba9780b..8ff92b6c 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -1,5 +1,7 @@ -/* globals workbox */ +/* eslint-disable eslint-comments/disable-enable-pair */ /* eslint-disable no-restricted-globals */ +/* eslint-disable no-underscore-dangle */ +/* globals workbox */ workbox.core.setCacheNameDetails({ prefix: 'antd-pro', suffix: 'v1', @@ -11,7 +13,6 @@ workbox.clientsClaim(); * Use precaching list generated by workbox in build process. * https://developers.google.com/web/tools/workbox/reference-docs/latest/workbox.precaching */ -/* eslint-disable no-underscore-dangle */ workbox.precaching.precacheAndRoute(self.__precacheManifest || []); /** diff --git a/src/utils/Authorized.ts b/src/utils/Authorized.ts index 71ab611b..807fc1fe 100644 --- a/src/utils/Authorized.ts +++ b/src/utils/Authorized.ts @@ -1,10 +1,11 @@ -import { default as RenderAuthorize } from '@/components/Authorized'; +import RenderAuthorize from '@/components/Authorized'; import { getAuthority } from './authority'; - -let Authorized = RenderAuthorize(getAuthority()); // eslint-disable-line +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable import/no-mutable-exports */ +let Authorized = RenderAuthorize(getAuthority()); // Reload the rights component -const reloadAuthorized = () => { +const reloadAuthorized = (): void => { Authorized = RenderAuthorize(getAuthority()); }; diff --git a/src/utils/authority.test.ts b/src/utils/authority.test.ts index 64012917..8222cb29 100644 --- a/src/utils/authority.test.ts +++ b/src/utils/authority.test.ts @@ -1,9 +1,8 @@ -import 'jest'; import { getAuthority } from './authority'; describe('getAuthority should be strong', () => { it('empty', () => { - expect(getAuthority(null)).toEqual(null); // default value + expect(getAuthority(null as any)).toEqual(null); // default value }); it('string', () => { expect(getAuthority('admin')).toEqual(['admin']); diff --git a/src/utils/authority.ts b/src/utils/authority.ts index bf0fa806..5fb029e0 100644 --- a/src/utils/authority.ts +++ b/src/utils/authority.ts @@ -6,7 +6,9 @@ export function getAuthority(str?: string): any { // authorityString could be admin, "admin", ["admin"] let authority; try { - authority = JSON.parse(authorityString!); + if (authorityString) { + authority = JSON.parse(authorityString); + } } catch (e) { authority = authorityString; } diff --git a/src/utils/request.ts b/src/utils/request.ts index 12859600..3babf7e7 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -5,12 +5,6 @@ import { extend } from 'umi-request'; import { notification } from 'antd'; -interface ResponseError extends Error { - name: string; - data: D; - response: Response; -} - const codeMessage = { 200: '服务器成功返回请求的数据。', 201: '新建或修改数据成功。', @@ -32,15 +26,17 @@ const codeMessage = { /** * 异常处理程序 */ -const errorHandler = (error: ResponseError) => { - const { response = {} as Response } = error; - const errortext = codeMessage[response.status] || response.statusText; - const { status, url } = response; +const errorHandler = (error: { response: Response }): void => { + const { response } = error; + if (response && response.status) { + const errorText = codeMessage[response.status] || response.statusText; + const { status, url } = response; - notification.error({ - message: `请求错误 ${status}: ${url}`, - description: errortext, - }); + notification.error({ + message: `请求错误 ${status}: ${url}`, + description: errorText, + }); + } }; /** diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index 3828b880..c2bb32dc 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -1,8 +1,7 @@ -import 'jest'; import { isUrl } from './utils'; -describe('isUrl tests', () => { - it('should return false for invalid and corner case inputs', () => { +describe('isUrl tests', (): void => { + it('should return false for invalid and corner case inputs', (): void => { expect(isUrl([] as any)).toBeFalsy(); expect(isUrl({} as any)).toBeFalsy(); expect(isUrl(false as any)).toBeFalsy(); @@ -13,7 +12,7 @@ describe('isUrl tests', () => { expect(isUrl('')).toBeFalsy(); }); - it('should return false for invalid URLs', () => { + it('should return false for invalid URLs', (): void => { expect(isUrl('foo')).toBeFalsy(); expect(isUrl('bar')).toBeFalsy(); expect(isUrl('bar/test')).toBeFalsy(); @@ -21,7 +20,7 @@ describe('isUrl tests', () => { expect(isUrl('ttp://example.com/')).toBeFalsy(); }); - it('should return true for valid URLs', () => { + it('should return true for valid URLs', (): void => { expect(isUrl('http://example.com/')).toBeTruthy(); expect(isUrl('https://example.com/')).toBeTruthy(); expect(isUrl('http://example.com/test/123')).toBeTruthy(); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 480064c3..5516e12e 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,12 +1,12 @@ /* 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: string) { +export function isUrl(path: string): string { return reg.test(path); } // 给官方演示站点用,用于关闭真实开发环境不需要使用的特性 -export function isAntDesignProOrDev() { +export function isAntDesignProOrDev(): boolean { const { NODE_ENV } = process.env; if (ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site') { return true; diff --git a/tests/run-tests.js b/tests/run-tests.js index 5735ada4..ea531ef1 100644 --- a/tests/run-tests.js +++ b/tests/run-tests.js @@ -1,4 +1,6 @@ -/* eslint-disable no-console */ +/* eslint-disable eslint-comments/disable-enable-pair */ +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-disable eslint-comments/no-unlimited-disable */ const { spawn } = require('child_process'); const { kill } = require('cross-port-killer'); @@ -37,7 +39,7 @@ startServer.stdout.on('data', data => { ['test', '--', '--maxWorkers=1', '--runInBand'], { stdio: 'inherit', - } + }, ); testCmd.on('exit', code => { startServer.kill(); diff --git a/tslint.yml b/tslint.yml deleted file mode 100644 index 7f4cc04d..00000000 --- a/tslint.yml +++ /dev/null @@ -1,88 +0,0 @@ -defaultSeverity: error -globals: - - ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true -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-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