提交 efa47a3a 编写于 作者: A Artyom Zankevich 提交者: Boris Sekachev

[NEW-UI] Delete task and pagination edge-cases (#599)

上级 f341419f
......@@ -10,6 +10,7 @@
"@types/react-dom": "16.8.4",
"@types/react-redux": "^7.1.1",
"@types/react-router-dom": "^4.3.4",
"@types/redux-logger": "^3.0.7",
"antd": "^3.19.1",
"babel-plugin-import": "^1.11.2",
"customize-cra": "^0.2.12",
......@@ -24,6 +25,7 @@
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"redux": "^4.0.3",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"source-map-explorer": "^1.8.0",
"typescript": "3.4.5"
......
......@@ -17,13 +17,15 @@ export const loginError = (error = {}) => (dispatch: any) => {
});
}
export const loginAsync = (username: string, password: string) => {
export const loginAsync = (username: string, password: string, history: any) => {
return (dispatch: any) => {
dispatch(login());
return (window as any).cvat.server.login(username, password).then(
(authenticated: any) => {
localStorage.setItem('session', 'true');
dispatch(loginSuccess());
history.push(history.location.state ? history.location.state.from : '/dashboard');
},
(error: any) => {
dispatch(loginError(error));
......
import queryString from 'query-string';
import setQueryObject from '../utils/tasks-filter-dto'
export const getTasks = () => (dispatch: any, getState: any) => {
dispatch({
type: 'GET_TASKS',
......@@ -52,14 +57,31 @@ export const getTasksAsync = (queryObject = {}) => {
};
}
export const deleteTaskAsync = (task: any) => {
return (dispatch: any) => {
export const deleteTaskAsync = (task: any, history: any) => {
return (dispatch: any, getState: any) => {
dispatch(deleteTask());
return task.delete().then(
(deleted: any) => {
dispatch(deleteTaskSuccess());
dispatch(getTasksAsync());
const state = getState();
const queryObject = {
page: state.tasksFilter.currentPage,
search: state.tasksFilter.searchQuery,
}
if (state.tasks.tasks.length === 1 && state.tasks.tasksCount !== 1) {
queryObject.page = queryObject.page - 1;
history.push({ search: queryString.stringify(queryObject) });
} else if (state.tasks.tasksCount === 1) {
dispatch(getTasksAsync());
} else {
const query = setQueryObject(queryObject);
dispatch(getTasksAsync(query));
}
},
(error: any) => {
dispatch(deleteTaskError(error));
......
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
......
......@@ -2,34 +2,47 @@ import React, { PureComponent } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { loginAsync } from '../../actions/auth.actions';
import Dashboard from '../dashboard/dashboard';
import Login from '../login/login';
import NotFound from '../not-found/not-found';
import DashboardPage from '../dashboard-page/dashboard-page';
import LoginPage from '../login-page/login-page';
import RegisterPage from '../register-page/register-page';
import PageNotFound from '../page-not-found/page-not-found';
import './app.scss';
class App extends PureComponent<any, any> {
componentDidMount() {
// TODO: remove when proper login flow (with router) will be implemented
this.props.dispatch(
loginAsync(
process.env.REACT_APP_LOGIN as string,
process.env.REACT_APP_PASSWORD as string,
),
);
}
const ProtectedRoute = ({ component: Component, ...rest }: any) => {
return (
<Route
{ ...rest }
render={ (props) => {
return localStorage.getItem('session') ? (
<Component { ...props } />
) : (
<Redirect
to={{
pathname: "/login",
state: {
from: props.location,
},
}}
/>
);
} }
/>
);
};
class App extends PureComponent<any, any> {
render() {
return(
<Router>
<Switch>
<Redirect path="/" exact to="/dashboard" />
<Route path="/dashboard" component={ Dashboard } />
<Route path="/login" component={ Login } />
<Route component={ NotFound } />
<ProtectedRoute path="/dashboard" component={ DashboardPage } />
<Route path="/login" component={ LoginPage } />
<Route path="/register" component={ RegisterPage } />
<Route component={ PageNotFound } />
</Switch>
</Router>
);
......
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardContent from './dashboard-content';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardContent />, div);
......
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { deleteTaskAsync } from '../../../actions/tasks.actions';
......@@ -120,7 +122,7 @@ class DashboardContent extends Component<any, any> {
okType: 'danger',
centered: true,
onOk() {
return self.props.dispatch(deleteTaskAsync(task));
return self.props.dispatch(deleteTaskAsync(task, self.props.history));
},
cancelText: 'No',
onCancel() {
......@@ -142,4 +144,4 @@ const mapStateToProps = (state: any) => {
return state.tasks;
};
export default connect(mapStateToProps)(DashboardContent);
export default withRouter(connect(mapStateToProps)(DashboardContent) as any);
import React from 'react';
import ReactDOM from 'react-dom';
import Dashboard from './dashboard';
import Dashboard from './dashboard-page';
it('renders without crashing', () => {
const div = document.createElement('div');
......
......@@ -3,6 +3,8 @@ import { Location, Action } from 'history';
import * as queryString from 'query-string';
import setQueryObject from '../../utils/tasks-filter-dto'
import { connect } from 'react-redux';
import { getTasksAsync } from '../../actions/tasks.actions';
import { filterTasks } from '../../actions/tasks-filter.actions';
......@@ -13,7 +15,8 @@ import DashboardHeader from './header/dashboard-header';
import DashboardContent from './content/dashboard-content';
import DashboardFooter from './footer/dashboard-footer';
import './dashboard.scss';
import './dashboard-page.scss';
class Dashboard extends PureComponent<any, any> {
componentDidMount() {
......@@ -21,7 +24,7 @@ class Dashboard extends PureComponent<any, any> {
this.props.history.listen((location: Location, action: Action) => {
this.loadTasks(location.search);
})
});
}
render() {
......@@ -36,25 +39,11 @@ class Dashboard extends PureComponent<any, any> {
private loadTasks = (params: any) => {
const query = queryString.parse(params);
const queryObject = this.setQueryObject(query);
const queryObject = setQueryObject(query);
this.props.dispatch(filterTasks(queryObject));
this.props.dispatch(getTasksAsync(queryObject));
}
private setQueryObject = (params: { search?: string, page?: string }): { search?: string, page?: number } => {
const queryObject: { search?: string, page?: number } = {};
if (params['search']) {
queryObject.search = params.search.toString();
}
if (params['page']) {
queryObject.page = parseInt(params.page);
}
return queryObject;
}
}
const mapStateToProps = (state: any) => {
......
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardFooter from './dashboard-footer';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardFooter />, div);
......
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardHeader from './dashboard-header';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardHeader />, div);
......
import React from 'react';
import ReactDOM from 'react-dom';
import Login from './login';
import Login from './login-page';
it('renders without crashing', () => {
const div = document.createElement('div');
......
......@@ -4,18 +4,24 @@ import { connect } from 'react-redux';
import { loginAsync } from '../../actions/auth.actions';
import { Button, Icon, Input, Form, Col, Row } from 'antd';
import './login.scss';
import Title from 'antd/lib/typography/Title';
import './login-page.scss';
class LoginForm extends PureComponent<any, any> {
componentWillMount() {
if (localStorage.getItem('session')) {
this.props.history.push('/dashboard');
}
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Row type="flex" justify="center" align="middle">
<Col span={4}>
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="login-form" onSubmit={ this.onSubmit }>
<Title className="login-form__title">Login</Title>
......@@ -63,7 +69,7 @@ class LoginForm extends PureComponent<any, any> {
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
this.props.dispatch(loginAsync(values.username, values.password))
this.props.dispatch(loginAsync(values.username, values.password, this.props.history));
}
});
}
......
import React from 'react';
import ReactDOM from 'react-dom';
import NotFound from './not-found';
import NotFound from './page-not-found';
it('renders without crashing', () => {
const div = document.createElement('div');
......
......@@ -2,9 +2,9 @@ import React, { PureComponent } from 'react';
import { Empty, Button } from 'antd';
import './not-found.scss';
import './page-not-found.scss';
class NotFound extends PureComponent<any, any> {
class PageNotFound extends PureComponent<any, any> {
render() {
return(
<Empty className="not-found" description="Page not found...">
......@@ -16,4 +16,4 @@ class NotFound extends PureComponent<any, any> {
}
}
export default NotFound;
export default PageNotFound;
.register-form {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
&__title {
}
}
import React from 'react';
import ReactDOM from 'react-dom';
import RegisterPage from './register-page';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<RegisterPage />, div);
ReactDOM.unmountComponentAtNode(div);
});
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
// import { registerAsync } from '../../actions/auth.actions';
import { Button, Icon, Input, Form, Col, Row } from 'antd';
import Title from 'antd/lib/typography/Title';
import './register-page.scss';
class RegisterForm extends PureComponent<any, any> {
constructor(props: any) {
super(props);
this.state = { confirmDirty: false };
}
componentWillMount() {
if (localStorage.getItem('session')) {
this.props.history.push('/dashboard');
}
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Row type="flex" justify="center" align="middle">
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="register-form" onSubmit={ this.onSubmit }>
<Title className="register-form__title">Register</Title>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please enter your username!' }],
})(
<Input
prefix={ <Icon type="user" /> }
type="text"
name="username"
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('firstName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="first-name"
placeholder="First name"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('lastName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="last-name"
placeholder="Last name"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('email', {
rules: [
{
type: 'email',
message: 'The input is not valid email!',
},
{
required: true,
message: 'Please input your email!',
},
],
})(
<Input
prefix={ <Icon type="mail" /> }
type="text"
name="email"
placeholder="Email"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})(
<Input.Password
prefix={ <Icon type="lock" /> }
name="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('passwordConfirmation', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})(
<Input.Password
onBlur={ this.handleConfirmBlur }
prefix={ <Icon type="lock" /> }
name="password-confirmation"
placeholder="Password confirmation"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={ this.props.isFetching }>
Register
</Button>
</Form.Item>
</Form>
</Col>
</Row>
);
}
private handleConfirmBlur = (event: any) => {
const { value } = event.target;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
};
private compareToFirstPassword = (rule: any, value: string, callback: Function) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter are inconsistent!');
} else {
callback();
}
};
private validateToNextPassword = (rule: any, value: string, callback: Function) => {
const { form } = this.props;
if (value && this.state.confirmDirty) {
form.validateFields(['passwordConfirmation'], { force: true });
}
callback();
};
private onSubmit = (event: any) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
// this.props.dispatch(registerAsync(values.username, values.password, this.props.history));
}
});
}
}
const mapStateToProps = (state: any) => {
return state.authContext;
};
export default Form.create()(connect(mapStateToProps)(RegisterForm));
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { Provider } from 'react-redux'
import configureStore from './store';
import App from './components/app/app';
import * as serviceWorker from './serviceWorker';
import './index.scss';
import App from './components/app/app';
ReactDOM.render(
<Provider store={ configureStore() }>
......
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger'
import rootReducer from './reducers/root.reducer';
export default function configureStore(initialState = {}) {
const logger = createLogger({
collapsed: true,
});
const middlewares = [];
if (process.env.NODE_ENV === `development`) {
middlewares.push(logger);
}
middlewares.push(thunk);
return createStore(
rootReducer,
initialState,
compose(
applyMiddleware(thunk),
applyMiddleware(...middlewares),
(window as any).__REDUX_DEVTOOLS_EXTENSION__
?
(window as any).__REDUX_DEVTOOLS_EXTENSION__({ trace: true })
......
export default (params: { search?: string, page?: string }): { search?: string, page?: number } => {
const queryObject: { search?: string, page?: number } = {};
if (params['search']) {
queryObject.search = params.search.toString();
}
if (params['page']) {
queryObject.page = parseInt(params.page);
}
return queryObject;
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册