Commit 0170acb4 authored by dengxiaofeng's avatar dengxiaofeng

init project

parent a40fec0b
......@@ -8,4 +8,5 @@ stats.json
.DS_Store
npm-debug.log
.idea
src/umi
export default {
mode: 'site',
title: '运维平台',
dynamicImport: {},
manifest: {},
hash: true,
resolve: {
includes: ['docs'],
},
styles: [
`a[title='站长统计'] { display: none; }`,
],
menus: {
'/guide': [
{
title: '介绍',
children: ['guide/index', 'guide/getting-started', 'guide/command'],
},
],
},
navs: [
null,
{ title: 'GitHub', path: 'https://github.com/umijs/dumi' },
{ title: '更新日志', path: 'https://github.com/umijs/dumi/releases' },
],
headScripts: [
'https://v1.cnzz.com/z_stat.php?id=1278602214&web_id=1278602214'
],
}
The MIT License (MIT)
Copyright (c) 2019 Maximilian Stoiber
Copyright (c) 2020 Maximilian Stoiber
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
......
This diff is collapsed.
......@@ -12,7 +12,7 @@ module.exports = {
'styled-components',
'@babel/plugin-proposal-class-properties',
'@babel/plugin-syntax-dynamic-import',
['import', { libraryName: 'antd', style: true }],
['import', { libraryName: 'antd', style: true }]
],
env: {
production: {
......
---
title: 基础组件
nav:
title: 组件
order: 1
---
\ No newline at end of file
---
title: 常见问题
nav:
title: faq
order: 3
---
\ No newline at end of file
---
title: script 命令
nav:
title: 指南
order: 1
---
# script 命令
## 初始化
```Shell
npm run setup
```
初始化一个新项目, 安装依赖项并初始化
## 开发环境
```Shell
npm run start
```
Starts the development server running on `http://localhost:3000`
## 清除
```Shell
npm run clean
```
删除应用代码,开始编写新的应用代码
## 生成模版代码块
```Shell
npm run generate
```
允许自动生成通用代码的样板代码应用, `component`, `container`<br>
执行 `npm run generate <part>`, 选择
## 服务
### 开发环境
```Shell
npm start
```
Starts the development server running on http://localhost:3000. 代码中的更改将被热加载.
### 生产环境
```Shell
npm run start:production
```
- 执行自动化测试脚本 (see `npm test`)
- 执行打包脚本 (see `npm run build`)
- 执行生产环境打包 (see `npm run start:prod`)
### Host and Port
更改ip或端口访问, 通过参数 `--host``--port` 命令行参数<br>
`npm start -- --host 127.0.0.1 --port 5000`
## 打包
```Shell
npm run build
```
## Testing
See the [testing documentation](../testing/README.md) for detailed information
about our testing setup!
## 单元测试
```Shell
npm test
```
### Watching
```Shell
npm run test:watch
```
## 检查代码规范
```Shell
npm run lint
```
```Shell
npm run lint:eslint:fix -- .
```
\ No newline at end of file
---
title: 快速上手
order: 9
nav:
order: 10
---
## 环境准备
首先得有 [node](https://nodejs.org/en/),并确保 node 版本是 10.13 或以上。
```bash
$ node -v
v10.13.0
```
## 脚手架初始化
---
title: 介绍
nav:
title: 指南
order: 1
---
### 技术栈
这是一个基本软件包列表,在您开始初始化项目之前,至少熟悉这些依赖包。 但是,查看依赖项完整列表的最佳方法是检查package.json
### Core
- [ ] [React](https://facebook.github.io/react/)
- [ ] [React Router](https://github.com/ReactTraining/react-router)
- [ ] [Redux](http://redux.js.org/)
- [ ] [Redux Saga](https://redux-saga.github.io/redux-saga/)
- [ ] [Reselect](https://github.com/reactjs/reselect)
- [ ] [Immer](https://github.com/mweststrate/immer)
- [ ] [Styled Components](https://github.com/styled-components/styled-components)
### UI
- [ ] [Ant-Design](https://github.com/ant-design/ant-design)
- [ ] [pro-components](https://github.com/ant-design/pro-components)
### Unit Testing
- [ ] [Jest](http://facebook.github.io/jest/)
- [ ] [react-testing-library](https://github.com/kentcdodds/react-testing-library)
### Linting
- [ ] [ESLint](http://eslint.org/)
- [ ] [Prettier](https://prettier.io/)
- [ ] [stylelint](https://stylelint.io/)
## 项目结构
- docs
- internals
- server
- src
- components
- containers
- layouts
- pages
- routes
- utils
\ No newline at end of file
---
title: 运维平台 - 开发文档
order: 10
hero:
title: 运维平台
actions:
- text: 快速开始
link: /guide/getting-started
footer: Open-source MIT Licensed | Copyright © 2020-present<br />Powered by self
---
<br>
## 快速开始
```bash
// 拉取代码
$ git clone http://xxx.com
// 安装
$ npm run setup
// 启动
$ npm start
```
/**
* Component Generator
*/
/* eslint strict: ["off"] */
'use strict';
......
......@@ -15,7 +15,7 @@ process.stdout.write('Cleanup started...');
// Cleanup components/
shell.rm('-rf', 'src/components/*');
// shell.rm('-rf', 'src/components/*');
// Handle containers/
shell.rm('-rf', 'src/containers');
......@@ -35,7 +35,7 @@ shell.mv('internals/templates/utils', 'app');
// Replace the files in the root app/ folder
shell.cp('internals/templates/app.js', 'src/app.js');
shell.cp('internals/templates/global-styles.js', 'src/global-styles.js');
shell.cp('internals/templates/i18n.js', 'src/i18n.js');
// shell.cp('internals/templates/i18n.js', 'src/i18n.js');
shell.cp('internals/templates/index.html', 'src/index.html');
shell.cp('internals/templates/reducers.js', 'src/reducers.js');
shell.cp('internals/templates/configureStore.js', 'src/configureStore.js');
......
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
// Needed for redux-saga es6 generator support
import '@babel/polyfill';
// Import all the third party stuff
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'connected-react-router';
import history from 'utils/history';
import 'sanitize.css/sanitize.css';
// Import root app
import App from 'containers/App';
// Import Language Provider
import LanguageProvider from 'containers/LanguageProvider';
// Load the favicon and the .htaccess file
/* eslint-disable import/no-unresolved, import/extensions */
import '!file-loader?name=[name].[ext]!./images/favicon.ico';
import 'file-loader?name=.htaccess!./.htaccess';
/* eslint-enable import/no-unresolved, import/extensions */
import configureStore from './configureStore';
// Import i18n messages
import { translationMessages } from './i18n';
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');
const render = messages => {
ReactDOM.render(
<Provider store={store}>
<LanguageProvider messages={messages}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</LanguageProvider>
</Provider>,
MOUNT_NODE,
);
};
if (module.hot) {
// Hot reloadable React components and translation json files
// modules.hot.accept does not accept dynamic dependencies,
// have to be constants at compile-time
module.hot.accept(['./i18n', 'containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render(translationMessages);
});
}
// Chunked polyfill for browsers without Intl support
if (!window.Intl) {
new Promise(resolve => {
resolve(import('intl'));
})
.then(() => Promise.all([import('intl/locale-data/jsonp/en.js')]))
.then(() => render(translationMessages))
.catch(err => {
throw err;
});
} else {
render(translationMessages);
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
if (process.env.NODE_ENV === 'production') {
require('offline-plugin/runtime').install(); // eslint-disable-line global-require
}
/**
* Create the store with dynamic reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'connected-react-router';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
export default function configureStore(initialState = {}, history) {
let composeEnhancers = compose;
const reduxSagaMonitorOptions = {};
// If Redux Dev Tools and Saga Dev Tools Extensions are installed, enable them
/* istanbul ignore next */
if (process.env.NODE_ENV !== 'production' && typeof window === 'object') {
/* eslint-disable no-underscore-dangle */
if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__)
composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({});
// NOTE: Uncomment the code below to restore support for Redux Saga
// Dev Tools once it supports redux-saga version 1.x.x
// if (window.__SAGA_MONITOR_EXTENSION__)
// reduxSagaMonitorOptions = {
// sagaMonitor: window.__SAGA_MONITOR_EXTENSION__,
// };
/* eslint-enable */
}
const sagaMiddleware = createSagaMiddleware(reduxSagaMonitorOptions);
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [sagaMiddleware, routerMiddleware(history)];
const enhancers = [applyMiddleware(...middlewares)];
const store = createStore(
createReducer(),
initialState,
composeEnhancers(...enhancers),
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.injectedReducers = {}; // Reducer registry
store.injectedSagas = {}; // Saga registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(createReducer(store.injectedReducers));
});
}
return store;
}
/*
* AppConstants
* Each action has a corresponding type, which the reducer knows and picks up on.
* To avoid weird typos between the reducer and the actions, we save them as
* constants here. We prefix them with 'yourproject/YourComponent' so we avoid
* reducers accidentally picking up actions they shouldn't.
*
* Follow this format:
* export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT';
*/
/**
*
* App.js
*
* This component is the skeleton around the actual pages, and should only
* contain code that should be seen on all pages. (e.g. navigation bar)
*
*/
import React from 'react';
import { Switch, Route } from 'react-router-dom';
import HomePage from 'containers/HomePage/Loadable';
import NotFoundPage from 'containers/NotFoundPage/Loadable';
import GlobalStyle from '../../global-styles';
export default function App() {
return (
<div>
<Switch>
<Route exact path="/" component={HomePage} />
<Route component={NotFoundPage} />
</Switch>
<GlobalStyle />
</div>
);
}
import { createSelector } from 'reselect';
const selectRouter = state => state.router;
const makeSelectLocation = () =>
createSelector(
selectRouter,
routerState => routerState.location,
);
export { makeSelectLocation };
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<App /> should render and match the snapshot 1`] = `
<div>
<Switch>
<Route
component={[Function]}
exact={true}
path="/"
/>
<Route
component={[Function]}
/>
</Switch>
<GlobalStyleComponent />
</div>
`;
import React from 'react';
import ShallowRenderer from 'react-test-renderer/shallow';
import App from '../index';
const renderer = new ShallowRenderer();
describe('<App />', () => {
it('should render and match the snapshot', () => {
renderer.render(<App />);
const renderedOutput = renderer.getRenderOutput();
expect(renderedOutput).toMatchSnapshot();
});
});
import { makeSelectLocation } from 'containers/App/selectors';
describe('makeSelectLocation', () => {
it('should select the location', () => {
const router = {
location: { pathname: '/foo' },
};
const mockedState = {
router,
};
expect(makeSelectLocation()(mockedState)).toEqual(router.location);
});
});
/**
* Asynchronously loads the component for HomePage
*/
import loadable from 'utils/loadable';
export default loadable(() => import('./index'));
/*
* HomePage
*
* This is the first thing users see of our App, at the '/' route
*
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
export default function HomePage() {
return (
<h1>
<FormattedMessage {...messages.header} />
</h1>
);
}
/*
* HomePage Messages
*
* This contains all the text for the HomePage container.
*/
import { defineMessages } from 'react-intl';
export const scope = 'app.containers.HomePage';
export default defineMessages({
header: {
id: `${scope}.header`,
defaultMessage: 'This is the HomePage container!',
},
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<HomePage /> should render and match the snapshot 1`] = `
<h1>
<span>
This is the HomePage container!
</span>
</h1>
`;
import React from 'react';
import { render } from 'react-testing-library';
import { IntlProvider } from 'react-intl';
import HomePage from '../index';
describe('<HomePage />', () => {
it('should render and match the snapshot', () => {
const {
container: { firstChild },
} = render(
<IntlProvider locale="en">
<HomePage />
</IntlProvider>,
);
expect(firstChild).toMatchSnapshot();
});
});
/*
*
* LanguageProvider actions
*
*/
import { CHANGE_LOCALE } from './constants';
export function changeLocale(languageLocale) {
return {
type: CHANGE_LOCALE,
locale: languageLocale,
};
}
/*
*
* LanguageProvider constants
*
*/
export const CHANGE_LOCALE = 'app/LanguageToggle/CHANGE_LOCALE';
/*
*
* LanguageProvider
*
* this component connects the redux state language locale to the
* IntlProvider component and i18n messages (loaded from `app/translations`)
*/
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { IntlProvider } from 'react-intl';
import { makeSelectLocale } from './selectors';
export function LanguageProvider(props) {
return (
<IntlProvider
locale={props.locale}
key={props.locale}
messages={props.messages[props.locale]}
>
{React.Children.only(props.children)}
</IntlProvider>
);
}
LanguageProvider.propTypes = {
locale: PropTypes.string,
messages: PropTypes.object,
children: PropTypes.element.isRequired,
};
const mapStateToProps = createSelector(
makeSelectLocale(),
locale => ({
locale,
}),
);
function mapDispatchToProps(dispatch) {
return {
dispatch,
};
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(LanguageProvider);
/*
*
* LanguageProvider reducer
*
*/
import produce from 'immer';
import { CHANGE_LOCALE } from './constants';
import { DEFAULT_LOCALE } from '../../i18n';
export const initialState = {
locale: DEFAULT_LOCALE,
};
/* eslint-disable default-case, no-param-reassign */
const languageProviderReducer = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case CHANGE_LOCALE:
draft.locale = action.locale;
break;
}
});
export default languageProviderReducer;
import { createSelector } from 'reselect';
import { initialState } from './reducer';
/**
* Direct selector to the languageToggle state domain
*/
const selectLanguage = state => state.language || initialState;
/**
* Select the language locale
*/
const makeSelectLocale = () =>
createSelector(
selectLanguage,
languageState => languageState.locale,
);
export { selectLanguage, makeSelectLocale };
/**
* Asynchronously loads the component for NotFoundPage
*/
import loadable from 'utils/loadable';
export default loadable(() => import('./index'));
/**
* NotFoundPage
*
* This is the page we show when the user visits a url that doesn't have a route
*
*/
import React from 'react';
import { FormattedMessage } from 'react-intl';
import messages from './messages';
export default function NotFound() {
return (
<h1>
<FormattedMessage {...messages.header} />
</h1>
);
}
/*
* NotFoundPage Messages
*
* This contains all the text for the NotFoundPage container.
*/
import { defineMessages } from 'react-intl';
export const scope = 'app.containers.NotFoundPage';
export default defineMessages({
header: {
id: `${scope}.header`,
defaultMessage: 'This is the NotFoundPage container!',
},
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<NotFoundPage /> should render and match the snapshot 1`] = `
<h1>
<span>
This is the NotFoundPage container!
</span>
</h1>
`;
import React from 'react';
import { render } from 'react-testing-library';
import { IntlProvider } from 'react-intl';
import NotFoundPage from '../index';
describe('<NotFoundPage />', () => {
it('should render and match the snapshot', () => {
const {
container: { firstChild },
} = render(
<IntlProvider locale="en">
<NotFoundPage />
</IntlProvider>,
);
expect(firstChild).toMatchSnapshot();
});
});
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
html,
body {
height: 100%;
width: 100%;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
body.fontLoaded {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
#app {
background-color: #fafafa;
min-height: 100%;
min-width: 100%;
}
p,
label {
font-family: Georgia, Times, 'Times New Roman', serif;
line-height: 1.5em;
}
`;
export default GlobalStyle;
<!doctype html>
<html lang="en">
<head>
<!-- The first thing in any HTML file should be the charset -->
<meta charset="utf-8">
<!-- Make the page mobile compatible -->
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Allow installing the app to the homescreen -->
<meta name="mobile-web-app-capable" content="yes">
<link rel="icon" href="/favicon.ico" />
<title>React.js Boilerplate</title>
</head>
<body>
<!-- Display a message if JS has been disabled on the browser. -->
<noscript>If you're seeing this message, that means
<strong>JavaScript has been disabled on your browser</strong>, please
<strong>enable JS</strong> to make this app work.</noscript>
<!-- The app hooks into this div -->
<div id="app"></div>
<!-- A lot of magic happens in this file. HtmlWebpackPlugin automatically injects all assets (e.g. bundle.js, main.css) with the correct HTML tags, which is why they are missing in this file. Don't add any assets here! (Check out webpack.dev.babel.js and webpack.prod.babel.js if you want to know more) -->
</body>
</html>
/**
* Combine all reducers in this file and export the combined reducers.
*/
import { combineReducers } from 'redux';
import { connectRouter } from 'connected-react-router';
import history from 'utils/history';
import languageProviderReducer from 'containers/LanguageProvider/reducer';
/**
* Merges the main reducer with the router state and dynamically injected reducers
*/
export default function createReducer(injectedReducers = {}) {
const rootReducer = combineReducers({
language: languageProviderReducer,
router: connectRouter(history),
...injectedReducers,
});
return rootReducer;
}
import { formatTranslationMessages } from '../i18n';
jest.mock('../translations/en.json', () => ({
message1: 'default message',
message2: 'default message 2',
}));
const esTranslationMessages = {
message1: 'mensaje predeterminado',
message2: '',
};
describe('formatTranslationMessages', () => {
it('should build only defaults when DEFAULT_LOCALE', () => {
const result = formatTranslationMessages('en', { a: 'a' });
expect(result).toEqual({ a: 'a' });
});
it('should combine default locale and current locale when not DEFAULT_LOCALE', () => {
const result = formatTranslationMessages('', esTranslationMessages);
expect(result).toEqual({
message1: 'mensaje predeterminado',
message2: 'default message 2',
});
});
});
/**
* Test store addons
*/
import { browserHistory } from 'react-router';
import configureStore from '../configureStore';
describe('configureStore', () => {
let store;
beforeAll(() => {
store = configureStore({}, browserHistory);
});
describe('injectedReducers', () => {
it('should contain an object for reducers', () => {
expect(typeof store.injectedReducers).toBe('object');
});
});
describe('injectedSagas', () => {
it('should contain an object for sagas', () => {
expect(typeof store.injectedSagas).toBe('object');
});
});
describe('runSaga', () => {
it('should contain a hook for `sagaMiddleware.run`', () => {
expect(typeof store.runSaga).toEqual('function');
});
});
});
import { conformsTo, isFunction, isObject } from 'lodash';
import invariant from 'invariant';
/**
* Validate the shape of redux store
*/
export default function checkStore(store) {
const shape = {
dispatch: isFunction,
subscribe: isFunction,
getState: isFunction,
replaceReducer: isFunction,
runSaga: isFunction,
injectedReducers: isObject,
injectedSagas: isObject,
};
invariant(
conformsTo(store, shape),
'(app/utils...) injectors: Expected a valid redux store',
);
}
export const RESTART_ON_REMOUNT = '@@saga-injector/restart-on-remount';
export const DAEMON = '@@saga-injector/daemon';
export const ONCE_TILL_UNMOUNT = '@@saga-injector/once-till-unmount';
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
export default history;
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';
import getInjectors from './reducerInjectors';
/**
* Dynamically injects a reducer
*
* @param {string} key A key of the reducer
* @param {function} reducer A reducer that will be injected
*
*/
export default ({ key, reducer }) => WrappedComponent => {
class ReducerInjector extends React.Component {
static WrappedComponent = WrappedComponent;
static contextType = ReactReduxContext;
static displayName = `withReducer(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
constructor(props, context) {
super(props, context);
getInjectors(context.store).injectReducer(key, reducer);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(ReducerInjector, WrappedComponent);
};
const useInjectReducer = ({ key, reducer }) => {
const context = React.useContext(ReactReduxContext);
React.useEffect(() => {
getInjectors(context.store).injectReducer(key, reducer);
}, []);
};
export { useInjectReducer };
import React from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics';
import { ReactReduxContext } from 'react-redux';
import getInjectors from './sagaInjectors';
/**
* Dynamically injects a saga, passes component's props as saga arguments
*
* @param {string} key A key of the saga
* @param {function} saga A root saga that will be injected
* @param {string} [mode] By default (constants.DAEMON) the saga will be started
* on component mount and never canceled or started again. Another two options:
* - constants.RESTART_ON_REMOUNT — the saga will be started on component mount and
* cancelled with `task.cancel()` on component unmount for improved performance,
* - constants.ONCE_TILL_UNMOUNT — behaves like 'RESTART_ON_REMOUNT' but never runs it again.
*
*/
export default ({ key, saga, mode }) => WrappedComponent => {
class InjectSaga extends React.Component {
static WrappedComponent = WrappedComponent;
static contextType = ReactReduxContext;
static displayName = `withSaga(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
constructor(props, context) {
super(props, context);
this.injectors = getInjectors(context.store);
this.injectors.injectSaga(key, { saga, mode }, this.props);
}
componentWillUnmount() {
this.injectors.ejectSaga(key);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(InjectSaga, WrappedComponent);
};
const useInjectSaga = ({ key, saga, mode }) => {
const context = React.useContext(ReactReduxContext);
React.useEffect(() => {
const injectors = getInjectors(context.store);
injectors.injectSaga(key, { saga, mode });
return () => {
injectors.ejectSaga(key);
};
}, []);
};
export { useInjectSaga };
import React, { lazy, Suspense } from 'react';
const loadable = (importFunc, { fallback = null } = { fallback: null }) => {
const LazyComponent = lazy(importFunc);
return props => (
<Suspense fallback={fallback}>
<LazyComponent {...props} />
</Suspense>
);
};
export default loadable;
import invariant from 'invariant';
import { isEmpty, isFunction, isString } from 'lodash';
import checkStore from './checkStore';
import createReducer from '../reducers';
export function injectReducerFactory(store, isValid) {
return function injectReducer(key, reducer) {
if (!isValid) checkStore(store);
invariant(
isString(key) && !isEmpty(key) && isFunction(reducer),
'(app/utils...) injectReducer: Expected `reducer` to be a reducer function',
);
// Check `store.injectedReducers[key] === reducer` for hot reloading when a key is the same but a reducer is different
if (
Reflect.has(store.injectedReducers, key) &&
store.injectedReducers[key] === reducer
)
return;
store.injectedReducers[key] = reducer; // eslint-disable-line no-param-reassign
store.replaceReducer(createReducer(store.injectedReducers));
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectReducer: injectReducerFactory(store, true),
};
}
import invariant from 'invariant';
import { isEmpty, isFunction, isString, conformsTo } from 'lodash';
import checkStore from './checkStore';
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from './constants';
const allowedModes = [RESTART_ON_REMOUNT, DAEMON, ONCE_TILL_UNMOUNT];
const checkKey = key =>
invariant(
isString(key) && !isEmpty(key),
'(app/utils...) injectSaga: Expected `key` to be a non empty string',
);
const checkDescriptor = descriptor => {
const shape = {
saga: isFunction,
mode: mode => isString(mode) && allowedModes.includes(mode),
};
invariant(
conformsTo(descriptor, shape),
'(app/utils...) injectSaga: Expected a valid saga descriptor',
);
};
export function injectSagaFactory(store, isValid) {
return function injectSaga(key, descriptor = {}, args) {
if (!isValid) checkStore(store);
const newDescriptor = {
...descriptor,
mode: descriptor.mode || DAEMON,
};
const { saga, mode } = newDescriptor;
checkKey(key);
checkDescriptor(newDescriptor);
let hasSaga = Reflect.has(store.injectedSagas, key);
if (process.env.NODE_ENV !== 'production') {
const oldDescriptor = store.injectedSagas[key];
// enable hot reloading of daemon and once-till-unmount sagas
if (hasSaga && oldDescriptor.saga !== saga) {
oldDescriptor.task.cancel();
hasSaga = false;
}
}
if (
!hasSaga ||
(hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)
) {
/* eslint-disable no-param-reassign */
store.injectedSagas[key] = {
...newDescriptor,
task: store.runSaga(saga, args),
};
/* eslint-enable no-param-reassign */
}
};
}
export function ejectSagaFactory(store, isValid) {
return function ejectSaga(key) {
if (!isValid) checkStore(store);
checkKey(key);
if (Reflect.has(store.injectedSagas, key)) {
const descriptor = store.injectedSagas[key];
if (descriptor.mode && descriptor.mode !== DAEMON) {
descriptor.task.cancel();
// Clean up in production; in development we need `descriptor.saga` for hot reloading
if (process.env.NODE_ENV === 'production') {
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
}
}
}
};
}
export default function getInjectors(store) {
checkStore(store);
return {
injectSaga: injectSagaFactory(store, true),
ejectSaga: ejectSagaFactory(store, true),
};
}
/**
* Test injectors
*/
import checkStore from '../checkStore';
describe('checkStore', () => {
let store;
beforeEach(() => {
store = {
dispatch: () => {},
subscribe: () => {},
getState: () => {},
replaceReducer: () => {},
runSaga: () => {},
injectedReducers: {},
injectedSagas: {},
};
});
it('should not throw if passed valid store shape', () => {
expect(() => checkStore(store)).not.toThrow();
});
it('should throw if passed invalid store shape', () => {
expect(() => checkStore({})).toThrow();
expect(() => checkStore({ ...store, injectedSagas: null })).toThrow();
expect(() => checkStore({ ...store, injectedReducers: null })).toThrow();
expect(() => checkStore({ ...store, runSaga: null })).toThrow();
expect(() => checkStore({ ...store, replaceReducer: null })).toThrow();
});
});
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { render } from 'react-testing-library';
import configureStore from '../../configureStore';
import injectReducer, { useInjectReducer } from '../injectReducer';
import * as reducerInjectors from '../reducerInjectors';
// Fixtures
const Component = () => null;
const reducer = s => s;
describe('injectReducer decorator', () => {
let store;
let injectors;
let ComponentWithReducer;
beforeAll(() => {
reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectReducer: jest.fn(),
};
ComponentWithReducer = injectReducer({ key: 'test', reducer })(Component);
reducerInjectors.default.mockClear();
});
it('should inject a given reducer', () => {
renderer.create(
<Provider store={store}>
<ComponentWithReducer />
</Provider>,
);
expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
});
it('should set a correct display name', () => {
expect(ComponentWithReducer.displayName).toBe('withReducer(Component)');
expect(
injectReducer({ key: 'test', reducer })(() => null).displayName,
).toBe('withReducer(Component)');
});
it('should propagate props', () => {
const props = { testProp: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithReducer {...props} />
</Provider>,
);
const {
props: { children },
} = renderedComponent.getInstance();
expect(children.props).toEqual(props);
});
});
describe('useInjectReducer hook', () => {
let store;
let injectors;
let ComponentWithReducer;
beforeAll(() => {
injectors = {
injectReducer: jest.fn(),
};
reducerInjectors.default = jest.fn().mockImplementation(() => injectors);
store = configureStore({}, memoryHistory);
ComponentWithReducer = () => {
useInjectReducer({ key: 'test', reducer });
return null;
};
});
it('should inject a given reducer', () => {
render(
<Provider store={store}>
<ComponentWithReducer />
</Provider>,
);
expect(injectors.injectReducer).toHaveBeenCalledTimes(1);
expect(injectors.injectReducer).toHaveBeenCalledWith('test', reducer);
});
});
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import { put } from 'redux-saga/effects';
import renderer from 'react-test-renderer';
import { render } from 'react-testing-library';
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../configureStore';
import injectSaga, { useInjectSaga } from '../injectSaga';
import * as sagaInjectors from '../sagaInjectors';
// Fixtures
const Component = () => null;
function* testSaga() {
yield put({ type: 'TEST', payload: 'yup' });
}
describe('injectSaga decorator', () => {
let store;
let injectors;
let ComponentWithSaga;
beforeAll(() => {
sagaInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectSaga: jest.fn(),
ejectSaga: jest.fn(),
};
ComponentWithSaga = injectSaga({
key: 'test',
saga: testSaga,
mode: 'testMode',
})(Component);
sagaInjectors.default.mockClear();
});
it('should inject given saga, mode, and props', () => {
const props = { test: 'test' };
renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
expect(injectors.injectSaga).toHaveBeenCalledTimes(1);
expect(injectors.injectSaga).toHaveBeenCalledWith(
'test',
{ saga: testSaga, mode: 'testMode' },
props,
);
});
it('should eject on unmount with a correct saga key', () => {
const props = { test: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
renderedComponent.unmount();
expect(injectors.ejectSaga).toHaveBeenCalledTimes(1);
expect(injectors.ejectSaga).toHaveBeenCalledWith('test');
});
it('should set a correct display name', () => {
expect(ComponentWithSaga.displayName).toBe('withSaga(Component)');
expect(
injectSaga({ key: 'test', saga: testSaga })(() => null).displayName,
).toBe('withSaga(Component)');
});
it('should propagate props', () => {
const props = { testProp: 'test' };
const renderedComponent = renderer.create(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
const {
props: { children },
} = renderedComponent.getInstance();
expect(children.props).toEqual(props);
});
});
describe('useInjectSaga hook', () => {
let store;
let injectors;
let ComponentWithSaga;
beforeAll(() => {
sagaInjectors.default = jest.fn().mockImplementation(() => injectors);
});
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectors = {
injectSaga: jest.fn(),
ejectSaga: jest.fn(),
};
ComponentWithSaga = () => {
useInjectSaga({
key: 'test',
saga: testSaga,
mode: 'testMode',
});
return null;
};
sagaInjectors.default.mockClear();
});
it('should inject given saga and mode', () => {
const props = { test: 'test' };
render(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
expect(injectors.injectSaga).toHaveBeenCalledTimes(1);
expect(injectors.injectSaga).toHaveBeenCalledWith('test', {
saga: testSaga,
mode: 'testMode',
});
});
it('should eject on unmount with a correct saga key', () => {
const props = { test: 'test' };
const { unmount } = render(
<Provider store={store}>
<ComponentWithSaga {...props} />
</Provider>,
);
unmount();
expect(injectors.ejectSaga).toHaveBeenCalledTimes(1);
expect(injectors.ejectSaga).toHaveBeenCalledWith('test');
});
});
/**
* Test injectors
*/
import produce from 'immer';
import { memoryHistory } from 'react-router-dom';
import identity from 'lodash/identity';
import configureStore from '../../configureStore';
import getInjectors, { injectReducerFactory } from '../reducerInjectors';
// Fixtures
const initialState = { reduced: 'soon' };
/* eslint-disable default-case, no-param-reassign */
const reducer = (state = initialState, action) =>
produce(state, draft => {
switch (action.type) {
case 'TEST':
draft.reduced = action.payload;
break;
}
});
describe('reducer injectors', () => {
let store;
let injectReducer;
describe('getInjectors', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
});
it('should return injectors', () => {
expect(getInjectors(store)).toEqual(
expect.objectContaining({
injectReducer: expect.any(Function),
}),
);
});
it('should throw if passed invalid store shape', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => getInjectors(store)).toThrow();
});
});
describe('injectReducer helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectReducer = injectReducerFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const inject = injectReducerFactory({});
expect(() => inject('test', reducer)).toThrow();
});
it('it should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => injectReducer('test', reducer)).not.toThrow();
});
it("should validate a reducer and reducer's key", () => {
expect(() => injectReducer('', reducer)).toThrow();
expect(() => injectReducer(1, reducer)).toThrow();
expect(() => injectReducer(1, 1)).toThrow();
});
it('given a store, it should provide a function to inject a reducer', () => {
injectReducer('test', reducer);
const actual = store.getState().test;
const expected = initialState;
expect(actual).toEqual(expected);
});
it('should not assign reducer if already existing', () => {
store.replaceReducer = jest.fn();
injectReducer('test', reducer);
injectReducer('test', reducer);
expect(store.replaceReducer).toHaveBeenCalledTimes(1);
});
it('should assign reducer if different implementation for hot reloading', () => {
store.replaceReducer = jest.fn();
injectReducer('test', reducer);
injectReducer('test', identity);
expect(store.replaceReducer).toHaveBeenCalledTimes(2);
});
});
});
/**
* Test injectors
*/
import { memoryHistory } from 'react-router-dom';
import { put } from 'redux-saga/effects';
import configureStore from '../../configureStore';
import getInjectors, {
injectSagaFactory,
ejectSagaFactory,
} from '../sagaInjectors';
import { DAEMON, ONCE_TILL_UNMOUNT, RESTART_ON_REMOUNT } from '../constants';
function* testSaga() {
yield put({ type: 'TEST', payload: 'yup' });
}
describe('injectors', () => {
const originalNodeEnv = process.env.NODE_ENV;
let store;
let injectSaga;
let ejectSaga;
describe('getInjectors', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
});
it('should return injectors', () => {
expect(getInjectors(store)).toEqual(
expect.objectContaining({
injectSaga: expect.any(Function),
ejectSaga: expect.any(Function),
}),
);
});
it('should throw if passed invalid store shape', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => getInjectors(store)).toThrow();
});
});
describe('ejectSaga helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectSaga = injectSagaFactory(store, true);
ejectSaga = ejectSagaFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const eject = ejectSagaFactory({});
expect(() => eject('test')).toThrow();
});
it('should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
injectSaga('test', { saga: testSaga });
expect(() => ejectSaga('test')).not.toThrow();
});
it("should validate saga's key", () => {
expect(() => ejectSaga('')).toThrow();
expect(() => ejectSaga(1)).toThrow();
});
it('should cancel a saga in RESTART_ON_REMOUNT mode', () => {
const cancel = jest.fn();
store.injectedSagas.test = { task: { cancel }, mode: RESTART_ON_REMOUNT };
ejectSaga('test');
expect(cancel).toHaveBeenCalled();
});
it('should not cancel a daemon saga', () => {
const cancel = jest.fn();
store.injectedSagas.test = { task: { cancel }, mode: DAEMON };
ejectSaga('test');
expect(cancel).not.toHaveBeenCalled();
});
it('should ignore saga that was not previously injected', () => {
expect(() => ejectSaga('test')).not.toThrow();
});
it("should remove non daemon saga's descriptor in production", () => {
process.env.NODE_ENV = 'production';
injectSaga('test', { saga: testSaga, mode: RESTART_ON_REMOUNT });
injectSaga('test1', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
ejectSaga('test');
ejectSaga('test1');
expect(store.injectedSagas.test).toBe('done');
expect(store.injectedSagas.test1).toBe('done');
process.env.NODE_ENV = originalNodeEnv;
});
it("should not remove daemon saga's descriptor in production", () => {
process.env.NODE_ENV = 'production';
injectSaga('test', { saga: testSaga, mode: DAEMON });
ejectSaga('test');
expect(store.injectedSagas.test.saga).toBe(testSaga);
process.env.NODE_ENV = originalNodeEnv;
});
it("should not remove daemon saga's descriptor in development", () => {
injectSaga('test', { saga: testSaga, mode: DAEMON });
ejectSaga('test');
expect(store.injectedSagas.test.saga).toBe(testSaga);
});
});
describe('injectSaga helper', () => {
beforeEach(() => {
store = configureStore({}, memoryHistory);
injectSaga = injectSagaFactory(store, true);
ejectSaga = ejectSagaFactory(store, true);
});
it('should check a store if the second argument is falsy', () => {
const inject = injectSagaFactory({});
expect(() => inject('test', testSaga)).toThrow();
});
it('it should not check a store if the second argument is true', () => {
Reflect.deleteProperty(store, 'dispatch');
expect(() => injectSaga('test', { saga: testSaga })).not.toThrow();
});
it("should validate saga's key", () => {
expect(() => injectSaga('', { saga: testSaga })).toThrow();
expect(() => injectSaga(1, { saga: testSaga })).toThrow();
});
it("should validate saga's descriptor", () => {
expect(() => injectSaga('test')).toThrow();
expect(() => injectSaga('test', { saga: 1 })).toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: 'testMode' }),
).toThrow();
expect(() => injectSaga('test', { saga: testSaga, mode: 1 })).toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: RESTART_ON_REMOUNT }),
).not.toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: DAEMON }),
).not.toThrow();
expect(() =>
injectSaga('test', { saga: testSaga, mode: ONCE_TILL_UNMOUNT }),
).not.toThrow();
});
it('should pass args to saga.run', () => {
const args = {};
store.runSaga = jest.fn();
injectSaga('test', { saga: testSaga }, args);
expect(store.runSaga).toHaveBeenCalledWith(testSaga, args);
});
it('should not start daemon and once-till-unmount sagas if were started before', () => {
store.runSaga = jest.fn();
injectSaga('test1', { saga: testSaga, mode: DAEMON });
injectSaga('test1', { saga: testSaga, mode: DAEMON });
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
injectSaga('test2', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
expect(store.runSaga).toHaveBeenCalledTimes(2);
});
it('should start any saga that was not started before', () => {
store.runSaga = jest.fn();
injectSaga('test1', { saga: testSaga });
injectSaga('test2', { saga: testSaga, mode: DAEMON });
injectSaga('test3', { saga: testSaga, mode: ONCE_TILL_UNMOUNT });
expect(store.runSaga).toHaveBeenCalledTimes(3);
});
it('should restart a saga if different implementation for hot reloading', () => {
const cancel = jest.fn();
store.injectedSagas.test = { saga: testSaga, task: { cancel } };
store.runSaga = jest.fn();
function* testSaga1() {
yield put({ type: 'TEST', payload: 'yup' });
}
injectSaga('test', { saga: testSaga1 });
expect(cancel).toHaveBeenCalledTimes(1);
expect(store.runSaga).toHaveBeenCalledWith(testSaga1, undefined);
});
it('should not cancel saga if different implementation in production', () => {
process.env.NODE_ENV = 'production';
const cancel = jest.fn();
store.injectedSagas.test = {
saga: testSaga,
task: { cancel },
mode: RESTART_ON_REMOUNT,
};
function* testSaga1() {
yield put({ type: 'TEST', payload: 'yup' });
}
injectSaga('test', { saga: testSaga1, mode: DAEMON });
expect(cancel).toHaveBeenCalledTimes(0);
process.env.NODE_ENV = originalNodeEnv;
});
it('should save an entire descriptor in the saga registry', () => {
injectSaga('test', { saga: testSaga, foo: 'bar' });
expect(store.injectedSagas.test.foo).toBe('bar');
});
});
});
const path = require('path');
const webpack = require('webpack');
const slash = require('slash2');
module.exports = options => ({
mode: options.mode,
entry: options.entry,
output: Object.assign(
{
// Compile into js/build.js
path: path.resolve(process.cwd(), 'build'),
publicPath: '/',
},
......@@ -16,13 +15,14 @@ module.exports = options => ({
module: {
rules: [
{
test: /\.jsx?$/, // Transform all .js and .jsx files required somewhere with Babel
test: /\.jsx?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: options.babelQuery,
},
},
{
// Preprocess our own .css files
// This is the place to add your own loaders (e.g. sass/less etc.)
......@@ -40,6 +40,36 @@ module.exports = options => ({
},
{
loader: 'css-loader',
options: {
modules: {
// localIdentName: '[name]__[local]___[hash:base64:5]'
// modules: true,
getLocalIdent: (context, _, localName) => {
if (
context.resourcePath.includes('node_modules') ||
context.resourcePath.includes('ant.design.pro.less') ||
context.resourcePath.includes('global.less')
) {
return localName;
}
const match = context.resourcePath.match(/src(.*)/);
if (match && match[1]) {
const antdProPath = match[1].replace('.less', '');
const arr = slash(antdProPath)
.split('/')
.map(a => a.replace(/([A-Z])/g, '-$1'))
.map(a => a.toLowerCase());
return `antd-pro${arr.join('-')}-${localName}`.replace(/--/g, '-');
}
return localName;
},
}
},
},
{
loader: 'less-loader',
......
......@@ -41,11 +41,16 @@ module.exports = require('./webpack.base.babel')({
failOnError: false, // show a warning when there is a circular dependency
}),
],
// Emit a source map for easier debugging
// See https://webpack.js.org/configuration/devtool/#devtool
devtool: 'eval-source-map',
devtool: 'cheap-module-source-map',
node: {
setImmediate: false,
process: 'mock',
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
},
performance: {
hints: false,
},
......
......@@ -25,6 +25,10 @@
"start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server",
"start:production": "npm run test && npm run build && npm run start:prod",
"start:prod": "cross-env NODE_ENV=production node server",
"release": "np --no-cleanup --yolo --no-publish",
"docs:dev": "dumi dev",
"docs:build": "dumi build",
"update:deps": "yarn upgrade-interactive --latest",
"presetup": "npm i chalk shelljs",
"setup": "node ./internals/scripts/setup.js",
"clean": "shjs ./internals/scripts/clean.js",
......@@ -48,6 +52,12 @@
"> 1%",
"IE 10"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged",
"pre-push": "npm test"
}
},
"lint-staged": {
"*.js": [
"npm run lint:eslint:fix",
......@@ -64,6 +74,7 @@
},
"dependencies": {
"@babel/polyfill": "7.4.3",
"@babel/runtime": "^7.10.5",
"chalk": "2.4.2",
"compression": "1.7.4",
"connected-react-router": "6.4.0",
......@@ -122,8 +133,10 @@
"classnames": "^2.2.6",
"compare-versions": "3.4.0",
"compression-webpack-plugin": "2.0.0",
"core-js": "^3.6.5",
"coveralls": "3.0.3",
"css-loader": "^4.2.1",
"dumi": "^1.0.13",
"eslint": "5.16.0",
"eslint-config-airbnb": "17.1.0",
"eslint-config-airbnb-base": "13.1.0",
......@@ -138,6 +151,7 @@
"file-loader": "3.0.1",
"html-loader": "0.5.5",
"html-webpack-plugin": "3.2.0",
"husky": "^2.3.0",
"image-webpack-loader": "4.6.0",
"imports-loader": "0.8.0",
"jest-cli": "24.7.1",
......@@ -154,6 +168,11 @@
"offline-plugin": "5.0.6",
"path-to-regexp": "2.4.0",
"plop": "2.3.0",
"postcss": "7.0.32",
"postcss-flexbugs-fixes": "4.2.1",
"postcss-loader": "3.0.0",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.2",
"pre-commit": "1.2.2",
"prettier": "1.17.0",
"qs": "^6.9.0",
......@@ -164,8 +183,10 @@
"react-router-config": "^5.1.1",
"react-test-renderer": "16.8.6",
"react-testing-library": "6.1.2",
"regenerator-runtime": "^0.13.7",
"rimraf": "2.6.3",
"shelljs": "0.8.3",
"slash2": "^2.0.0",
"style-loader": "^1.2.1",
"stylelint": "10.0.1",
"stylelint-config-recommended": "2.2.0",
......@@ -179,6 +200,7 @@
"webpack-dev-middleware": "3.6.2",
"webpack-hot-middleware": "2.24.3",
"webpack-pwa-manifest": "4.0.0",
"whatwg-fetch": "3.0.0"
"whatwg-fetch": "3.0.0",
"yorkie": "^2.0.0"
}
}
/**
* app.js
*
* This is the entry file for the application, only setup and boilerplate
* code.
*/
// Load the favicon and the .htaccess file
import '!file-loader?name=[name].[ext]!./images/favicon.ico';
// Needed for redux-saga es6 generator support
import './global.less';
import '@babel/polyfill';
import 'antd/dist/antd.less';
import 'file-loader?name=.htaccess!./.htaccess'; // eslint-disable-line import/extensions
import 'sanitize.css/sanitize.css';
......@@ -17,15 +10,12 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { ConnectedRouter } from 'connected-react-router';
// Import root app
import App from 'containers/App';
// Import Language Provider
import { Provider } from 'react-redux';
import history from 'utils/history';
import configureStore from './configureStore';
import App from './containers/App';
import history from './utils/history';
// Create redux store with history
const initialState = {};
const store = configureStore(initialState, history);
const MOUNT_NODE = document.getElementById('app');
......@@ -41,21 +31,16 @@ const render = () => {
);
};
// if (module.hot) {
// // Hot reloadable React components and translation json files
// // modules.hot.accept does not accept dynamic dependencies,
// // have to be constants at compile-time
// module.hot.accept(['containers/App'], () => {
// ReactDOM.unmountComponentAtNode(MOUNT_NODE);
// render();
// });
// }
render();
if (module.hot) {
module.hot.accept(['./containers/App'], () => {
ReactDOM.unmountComponentAtNode(MOUNT_NODE);
render();
});
}
if(MOUNT_NODE) {
render();
}
// Install ServiceWorker and AppCache in the end since
// it's not most important operation and if main code fails,
// we do not want it installed
if (process.env.NODE_ENV === 'production') {
require('offline-plugin/runtime').install(); // eslint-disable-line global-require
}
import './index.less';
import React from 'react';
import { Tooltip } from 'antd';
......@@ -9,21 +7,22 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import HeaderSearch from '../HeaderSearch';
import NoticeIcon from '../NoticeIcon';
import Avatar from './AvatarDropdown';
import styles from './index.less';
const GlobalHeaderRight = props => {
const { theme, layout } = props;
let className = 'ant-pro-components-global-header-index-right';
let className = styles.right;
if (theme === 'dark' && layout === 'top') {
className = `${className} `;
className = `${styles.right} ${styles.dark}`;
}
return (
<div className={className}>
<HeaderSearch
className="ant-pro-components-global-header-index-action ant-pro-components-global-header-index-search"
className={`${styles.action} ${styles.search}`}
placeholder="站内搜索"
defaultValue="umi ui"
defaultValue=""
options={[
{
label: <a href="https://umijs.org/zh/guide/umi-ui.html">umi ui</a>,
......@@ -51,12 +50,12 @@ const GlobalHeaderRight = props => {
target="_blank"
href="https://pro.ant.design/docs/getting-started"
rel="noopener noreferrer"
className="ant-pro-components-global-header-index-action"
className={styles.action}
>
<QuestionCircleOutlined />
</a>
</Tooltip>
<NoticeIcon className="ant-pro-components-global-header-index-action" />
<NoticeIcon className={styles.action} />
<Avatar />
{/* <SelectLang className={styles.action} /> */}
</div>
......
@import '~antd/es/style/themes/default.less';
@pro-header : ~'@{ant-prefix}-pro-components-global-header-index';
@pro-header-hover-bg: rgba(0, 0, 0, 0.025);
.menu {
......@@ -11,13 +10,13 @@
}
}
.@{pro-header}-right {
.right {
display: flex;
float: right;
height: 48px;
margin-left: auto;
overflow: hidden;
.@{pro-header}-action {
.action {
display: flex;
align-items: center;
height: 100%;
......@@ -34,13 +33,13 @@
background: @pro-header-hover-bg;
}
}
.@{pro-header}-search {
.search {
padding: 0 12px;
&:hover {
background: transparent;
}
}
.@{pro-header}-account {
.account {
.avatar {
margin: ~'calc((@{layout-header-height} - 24px) / 2)' 0;
margin-right: 8px;
......
import { Dropdown } from 'antd';
import React from 'react';
import { Dropdown } from 'antd';
import classNames from 'classnames';
import './index.less';
import styles from './index.less';
const HeaderDropdown = ({ overlayClassName: cls, ...restProps }) => (
<Dropdown
overlayClassName={classNames(
'ant-pro-components-header-dropdown-index-container',
styles.container,
cls,
)}
{...restProps}
......
@import '~antd/es/style/themes/default.less';
@pro-header-drop : ~'@{ant-prefix}-ant-pro-components-header-dropdown-index';
.@{pro-header-drop}-container > * {
.container > * {
background-color: @popover-bg;
border-radius: 4px;
box-shadow: @shadow-1-down;
}
@media screen and (max-width: @screen-xs) {
.@{pro-header-drop}-container {
.container {
width: 100% !important;
}
.@{pro-header-drop}-container > * {
.container > * {
border-radius: 0 !important;
}
}
\ No newline at end of file
import { SearchOutlined } from '@ant-design/icons';
import { AutoComplete, Input } from 'antd';
import useMergeValue from 'use-merge-value';
import React, { useRef } from 'react';
import PropType from 'prop-types';
import {
AutoComplete,
Input,
} from 'antd';
import classNames from 'classnames';
import './index.less';
import PropType from 'prop-types';
import useMergeValue from 'use-merge-value';
import { SearchOutlined } from '@ant-design/icons';
import styles from './index.less';
const HeaderSearch = props => {
const {
......@@ -31,9 +36,9 @@ const HeaderSearch = props => {
});
const inputClass = classNames(
'ant-pro-components-header-search-index-input',
styles.input,
{
'ant-pro-components-header-search-index-show': searchMode,
[styles.show]: searchMode,
},
);
......@@ -41,7 +46,7 @@ const HeaderSearch = props => {
<div
className={classNames(
className,
'ant-pro-components-header-search-index-headerSearch',
styles.headerSearch,
)}
onClick={() => {
setSearchMode(true);
......
@import '~antd/es/style/themes/default.less';
@pro-header-input : ~'@{ant-prefix}-pro-components-header-search-index';
.@{pro-header-input}-headerSearch {
.@{pro-header-input}-input {
.headerSearch {
.input {
width: 0;
min-width: 0;
overflow: hidden;
......@@ -23,7 +22,7 @@
&:focus {
border-bottom: 1px solid @border-color-base;
}
&.@{pro-header-input}-show {
&.show {
width: 210px;
margin-left: 8px;
}
......
import React from 'react';
import { Badge, Spin, Tabs } from 'antd';
import {
Badge,
Spin,
Tabs,
} from 'antd';
import classNames from 'classnames';
import useMergeValue from 'use-merge-value';
......@@ -76,18 +80,18 @@ const NoticeIcon = props => {
});
const noticeButtonClass = classNames(
className,
'ant-pro-components-notice-icon-index-noticeButton',
styles.noticeButton,
);
const notificationBox = getNotificationBox();
const NoticeBellIcon = bell || (
<BellOutlined className="ant-pro-components-notice-icon-index-icon" />
<BellOutlined className={styles.icon} />
);
const trigger = (
<span className={classNames(noticeButtonClass, { opened: visible })}>
<Badge
count={count}
style={{ boxShadow: 'none' }}
className="ant-pro-components-notice-icon-index-badge"
className={styles.badge}
>
{NoticeBellIcon}
</Badge>
......
@import '~antd/es/style/themes/default.less';
@pro-header-notice : ~'@{ant-prefix}-pro-components-notice-icon-index';
.popover {
position: relative;
width: 336px;
}
.@{pro-header-notice}-noticeButton {
.noticeButton {
display: inline-block;
cursor: pointer;
transition: all 0.3s;
}
.@{pro-header-notice}-icon {
.icon {
padding: 4px;
vertical-align: middle;
}
.@{pro-header-notice}-badge {
.badge {
font-size: 16px;
}
......
/*
* App Actions
*
* Actions change things in your application
* Since this boilerplate uses a uni-directional data flow, specifically redux,
* we have these actions which are the only way your application interacts with
* your application state. This guarantees that your state is up to date and nobody
* messes it up weirdly somewhere.
*
* To add a new Action:
* 1) Import your constant
* 2) Add a function like this:
* export function yourAction(var) {
* return { type: YOUR_ACTION_CONSTANT, var: var }
* }
*/
import {
LOAD_REPOS,
LOAD_REPOS_ERROR,
LOAD_REPOS_SUCCESS,
} from './constants';
import { LOAD_REPOS, LOAD_REPOS_SUCCESS, LOAD_REPOS_ERROR } from './constants';
/**
* Load the repositories, this action starts the request saga
*
* @return {object} An action object with a type of LOAD_REPOS
*/
export function loadRepos() {
return {
type: LOAD_REPOS,
};
}
/**
* Dispatched when the repositories are loaded by the request saga
*
* @param {array} repos The repository data
* @param {string} username The current username
*
* @return {object} An action object with a type of LOAD_REPOS_SUCCESS passing the repos
*/
export function reposLoaded(repos, username) {
return {
type: LOAD_REPOS_SUCCESS,
......@@ -44,13 +19,6 @@ export function reposLoaded(repos, username) {
};
}
/**
* Dispatched when loading the repositories fails
*
* @param {object} error The error
*
* @return {object} An action object with a type of LOAD_REPOS_ERROR passing the error
*/
export function repoLoadingError(error) {
return {
type: LOAD_REPOS_ERROR,
......
/*
* AppConstants
* Each action has a corresponding type, which the reducer knows and picks up on.
* To avoid weird typos between the reducer and the actions, we save them as
* constants here. We prefix them with 'yourproject/YourComponent' so we avoid
* reducers accidentally picking up actions they shouldn't.
*
* Follow this format:
* export const YOUR_ACTION_CONSTANT = 'yourproject/YourContainer/YOUR_ACTION_CONSTANT';
*/
export const LOAD_REPOS = 'boilerplate/App/LOAD_REPOS';
export const LOAD_REPOS_SUCCESS = 'boilerplate/App/LOAD_REPOS_SUCCESS';
export const LOAD_REPOS_ERROR = 'boilerplate/App/LOAD_REPOS_ERROR';
export const LOAD_REPOS = 'App/LOAD_REPOS';
export const LOAD_REPOS_SUCCESS = 'App/LOAD_REPOS_SUCCESS';
export const LOAD_REPOS_ERROR = 'App/LOAD_REPOS_ERROR';
import 'antd/dist/antd.less';
import React from 'react';
import { Helmet } from 'react-helmet';
import { renderRoutes } from 'react-router-config';
import { BrowserRouter as Router, Switch } from 'react-router-dom';
import {
BrowserRouter as Router,
Switch,
} from 'react-router-dom';
import GlobalStyle from '../../global-styles';
import config from '../../routes/config';
export default function App() {
return (
<>
<Helmet
titleTemplate="%s - React.js Boilerplate"
defaultTitle="React.js Boilerplate"
titleTemplate="%s - 运维平台"
defaultTitle="运维平台"
>
<meta name="description" content="A React.js Boilerplate application" />
<meta name="description" content="运维平台" />
</Helmet>
<Router>
<Switch>{renderRoutes(config.routes)}</Switch>
</Router>
<GlobalStyle />
</>
);
}
/*
* AppReducer
*
* The reducer takes care of our data. Using actions, we can
* update our application state. To add a new action,
* add it to the switch statement in the reducer function
*
*/
import produce from 'immer';
import { LOAD_REPOS_SUCCESS, LOAD_REPOS, LOAD_REPOS_ERROR } from './constants';
// The initial state of the App
import {
LOAD_REPOS,
LOAD_REPOS_ERROR,
LOAD_REPOS_SUCCESS,
} from './constants';
export const initialState = {
loading: false,
error: false,
......
/**
* The global state selectors
*/
import { createSelector } from 'reselect';
import { initialState } from './reducer';
const selectGlobal = state => state.global || initialState;
......@@ -40,10 +37,10 @@ const makeSelectLocation = () =>
);
export {
selectGlobal,
makeSelectCurrentUser,
makeSelectLoading,
makeSelectError,
makeSelectRepos,
makeSelectLoading,
makeSelectLocation,
makeSelectRepos,
selectGlobal,
};
import { createGlobalStyle } from 'styled-components';
const GlobalStyle = createGlobalStyle`
html,
body {
height: 100%;
width: 100%;
line-height: 1.5;
}
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
body.fontLoaded {
font-family: 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
html,body, #app {
height: 100%
}
#app {
background-color: #fafafa;
min-height: 100%;
min-width: 100%;
}
p,
label {
font-family: Georgia, Times, 'Times New Roman', serif;
line-height: 1.5em;
}
`;
export default GlobalStyle;
@import '~antd/es/style/themes/default.less';
html,
body,
#app {
height: 100%;
}
.colorWeak {
filter: invert(80%);
}
.ant-layout {
min-height: 100vh;
}
canvas {
display: block;
}
body {
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
ul,
ol {
list-style: none;
}
@media (max-width: @screen-xs) {
.ant-table {
width: 100%;
overflow-x: auto;
&-thead > tr,
&-tbody > tr {
> th,
> td {
white-space: pre;
> span {
display: block;
}
}
}
}
}
// 兼容IE11
@media screen and(-ms-high-contrast: active), (-ms-high-contrast: none) {
body .ant-design-pro > .ant-layout {
min-height: 100vh;
}
}
\ No newline at end of file
import './UserLayout.less';
import { renderRoutes } from 'react-router-config';
import React from 'react';
import {
Helmet,
HelmetProvider,
} from 'react-helmet-async';
import { renderRoutes } from 'react-router-config';
import { DefaultFooter } from '@ant-design/pro-layout';
import { Helmet, HelmetProvider } from 'react-helmet-async';
import logo from '../assets/logo.svg';
import styles from './UserLayout.less';
const UserLayout = props => {
const {
......@@ -15,38 +19,25 @@ const UserLayout = props => {
} = props;
// const { formatMessage } = useIntl();
// const { breadcrumb } = getMenuData(routes);
// const title = getPageTitle({
// pathname: location.pathname,
// // formatMessage,
// // breadcrumb,
// ...props,
// });
const title = ""
return (
<HelmetProvider>
<Helmet>
<title />
{/* <meta name="description" content={title} /> */}
<title>{title}</title>
<meta name="description" content={title} />
</Helmet>
<div className="ant-pro-layouts-user-layout-container">
<div className="ant-pro-layouts-user-layout-lang">
<div className={styles.container}>
<div className={styles.lang}>
{/* <SelectLang /> */}
</div>
<div className="ant-pro-layouts-user-layout-content">
<div className="ant-pro-layouts-user-layout-top">
<div className="ant-pro-layouts-user-layout-header">
<img
alt="logo"
className="ant-pro-layouts-user-layout-logo"
src={logo}
/>
<span className="ant-pro-layouts-user-layout-title">
Ant Design
</span>
</div>
<div className="ant-pro-layouts-user-layout-desc">
Ant Design 是西湖区最具影响力的 Web 设计规范
<div className={styles.content}>
<div className={styles.top}>
<div className={styles.header}>
<img alt="logo" className={styles.logo} src={logo} />
<span className={styles.title}>Ant Design</span>
</div>
<div className={styles.desc}>Ant Design 是西湖区最具影响力的 Web 设计规范</div>
</div>
{renderRoutes(route.routes)}
</div>
......
@import '~antd/es/style/themes/default.less';
@userLayout-prefix-cls: ~'@{ant-prefix}-pro-layouts-user-layout';
.@{userLayout-prefix-cls}-container {
.container {
display: flex;
flex-direction: column;
height: 100vh;
......@@ -9,7 +7,7 @@
background: @layout-body-background;
}
.@{userLayout-prefix-cls}-lang {
.lang {
width: 100%;
height: 40px;
line-height: 44px;
......@@ -19,29 +17,29 @@
}
}
.@{userLayout-prefix-cls}-content {
.content {
flex: 1;
padding: 32px 0;
}
@media (min-width: @screen-md-min) {
.@{userLayout-prefix-cls}-container {
.container {
background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
background-repeat: no-repeat;
background-position: center 110px;
background-size: 100%;
}
.@{userLayout-prefix-cls}-content {
.content {
padding: 32px 0 24px;
}
}
.@{userLayout-prefix-cls}-top {
.top {
text-align: center;
}
.@{userLayout-prefix-cls}-header {
.header {
height: 44px;
line-height: 44px;
a {
......@@ -49,13 +47,13 @@
}
}
.@{userLayout-prefix-cls}-logo {
.logo {
height: 44px;
margin-right: 16px;
vertical-align: top;
}
.@{userLayout-prefix-cls}-title {
.title {
position: relative;
top: 2px;
color: @heading-color;
......@@ -64,7 +62,7 @@
font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
}
.@{userLayout-prefix-cls}-desc {
.desc {
margin-top: 12px;
margin-bottom: 40px;
color: @text-color-secondary;
......
import { Button, Result } from 'antd';
import React from 'react';
const NoFoundPage: React.FC<{}> = () => (
import {
Button,
Result,
} from 'antd';
const NoFoundPage= () => (
<Result
status="404"
title="404"
......
import React from 'react';
import {
Button,
Result,
} from 'antd';
export default () => (
<Result
status="403"
title="403"
style={{
background: 'none',
}}
subTitle="Sorry, you don't have access to this page."
extra={
<Button type="primary">Back to home</Button>
}
/>
);
\ No newline at end of file
import React from 'react';
import {
Button,
Result,
} from 'antd';
export default () => (
<Result
status="404"
title="404"
style={{
background: 'none',
}}
subTitle="Sorry, the page you visited does not exist."
extra={
<Button type="primary">Back Home</Button>
}
/>
);
\ No newline at end of file
import React from 'react';
import { Form, Input } from 'antd';
import './index.less';
import {
Form,
Input,
} from 'antd';
import LoginContext from './LoginContext';
import ItemMap from './map';
......
import React from 'react';
import { Button, Form } from 'antd';
import {
Button,
Form,
} from 'antd';
import classNames from 'classnames';
import './index.less';
import styles from './index.less';
const FormItem = Form.Item;
const LoginSubmit = ({ className, ...rest }) => {
const clsString = classNames(
'ant-pro-pages-user-login-components-login-index-submit',
styles.submit,
className,
);
return (
......
import React from 'react';
import { Form } from 'antd';
import classNames from 'classnames';
import './index.less';
import styles from './index.less';
import LoginContext from './LoginContext';
import LoginItem from './LoginItem';
import LoginSubmit from './LoginSubmit';
......@@ -20,7 +22,7 @@ const Login = props => {
<div
className={classNames(
className,
'ant-pro-pages-user-login-components-login-index-login',
styles.login,
)}
>
<Form
......
@import '~antd/es/style/themes/default.less';
@userPageLoginForm-prefix-cls: ~'@{ant-prefix}-pro-pages-user-login';
.@{userPageLoginForm-prefix-cls}-index-login {
.login {
:global {
.ant-tabs .ant-tabs-bar {
margin-bottom: 24px;
......@@ -10,12 +9,12 @@
}
}
&-getCaptcha {
.getCaptcha {
display: block;
width: 100%;
}
&-icon {
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
......@@ -28,7 +27,7 @@
}
}
&-other {
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
......@@ -38,13 +37,13 @@
}
}
&-prefixIcon {
.prefixIcon {
color: @disabled-color;
font-size: @font-size-base;
}
}
.@{userPageLoginForm-prefix-cls}-components-login-index-submit {
.submit {
width: 100%;
margin-top: 24px;
}
}
\ No newline at end of file
import React, { useState } from 'react';
import { Checkbox, Alert } from 'antd';
import {
Alert,
Checkbox,
} from 'antd';
import LoginForm from './components/Login';
import './style.less';
import styles from './style.less';
const { UserName, Password, Submit } = LoginForm;
const LoginMessage = ({ content }) => (
......@@ -29,7 +33,7 @@ const Login = props => {
};
return (
<div className="ant-pro-pages-user-login-style-main">
<div className={styles.main}>
<LoginForm activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
{status === 'error' && loginType === 'account' && !submitting && (
<LoginMessage content="账户或密码错误(admin/ant.design)" />
......
@import '~antd/es/style/themes/default.less';
@userPageLogin-prefix-cls: ~'@{ant-prefix}-pro-pages-user-login';
.@{userPageLogin-prefix-cls}-style-main {
.main {
width: 368px;
margin: 0 auto;
@media screen and (max-width: @screen-sm) {
width: 95%;
}
.@{userPageLogin-prefix-cls}-icon {
.icon {
margin-left: 16px;
color: rgba(0, 0, 0, 0.2);
font-size: 24px;
......@@ -21,12 +21,12 @@
}
}
.@{userPageLogin-prefix-cls}-other {
.other {
margin-top: 24px;
line-height: 22px;
text-align: left;
.@{userPageLogin-prefix-cls}-register {
.register {
float: right;
}
}
......
......@@ -3,9 +3,10 @@
*/
import { connectRouter } from 'connected-react-router';
import globalReducer from 'containers/App/reducer';
import { combineReducers } from 'redux';
import history from 'utils/history';
import globalReducer from './containers/App/reducer';
import history from './utils/history';
/**
* Merges the main reducer with the router state and dynamically injected reducers
......
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