提交 3a91571b 编写于 作者: A Artyom Zankevich 提交者: Nikita Manovich

[NEW-UI] Add task filter reducer (#587)

* Case sensitive renaming
* Update `cvat-js` lib.
* Add `.env` file
* Add basic redux capabilities
* Remove `setTimeout` as it was fixes in `cvat-js`
* Remove redundant state field
* Add header and footer styles
* Remove `Affix`
* Add basic styles to task cards
* Fix empty component
* Pagination fixes
* Add modal on task delete
* Rename reducers
* Add task reducer
* Add lib. to parse query strings
* Minor refactoring
* Update `cvatjs`
* Add tasks filter reducer
* Do not make request with the same input value
* Fix some linter errors
* Refactor `dashboard-content` component
* Router changes
Redirect added.
Page not found added.
上级 ebbcb834
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
"less": "^3.9.0", "less": "^3.9.0",
"less-loader": "^5.0.0", "less-loader": "^5.0.0",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",
"query-string": "^6.8.1",
"react": "^16.8.6", "react": "^16.8.6",
"react-app-rewired": "^2.1.3", "react-app-rewired": "^2.1.3",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
......
因为 它太大了无法显示 source diff 。你可以改为 查看blob
export const login = (isAuthenticated: boolean) => (dispatch: any) => {
dispatch({
type: 'LOGIN',
payload: isAuthenticated,
});
}
export const logout = (isAuthenticated: boolean) => (dispatch: any) => {
dispatch({
type: 'LOGOUT',
payload: isAuthenticated,
});
}
export const loginAction = () => (dispatch: any) => {
dispatch({
type: 'LOGIN',
payload: true,
})
}
export const logoutAction = () => (dispatch: any) => {
dispatch({
type: 'LOGOUT',
payload: false,
})
}
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: any) => {
dispatch({
type: 'FILTER_TASKS',
payload: queryParams,
});
}
export const getTasks = (tasks: []) => (dispatch: any) => {
dispatch({
type: 'GET_TASKS',
payload: tasks,
});
}
export const getTasksAsync = (queryObject = {}) => {
return (dispatch: any) => {
return (window as any).cvat.tasks.get(queryObject).then(
(tasks: any) => {
dispatch(getTasks(tasks));
},
(error: any) => {
console.log(error);
},
);
};
}
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'; import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { login, logout } from '../../actions/auth.actions';
import Dashboard from '../dashboard/dashboard'; import Dashboard from '../dashboard/dashboard';
import NotFound from '../not-found/not-found';
import { loginAction, logoutAction } from '../../actions/authentication-action';
import './app.scss'; import './app.scss';
declare const window: any; declare const window: any;
const mapDispatchToProps = (dispatch: any) => ({
login: () => { dispatch(loginAction()) },
logout: () => { dispatch(logoutAction()) },
})
const mapStateToProps = (state: any) => ({
...state.authenticateReducer,
})
class App extends PureComponent<any, any> { class App extends PureComponent<any, any> {
componentDidMount() { componentDidMount() {
window.cvat.server.login(process.env.REACT_APP_LOGIN, process.env.REACT_APP_PASSWORD).then( window.cvat.server.login(process.env.REACT_APP_LOGIN, process.env.REACT_APP_PASSWORD).then(
(_response: any) => { (_response: any) => {
this.props.login(); this.props.dispatch(login(true));
}, },
(_error: any) => { (_error: any) => {
this.props.logout(); this.props.dispatch(logout(false));
} }
); );
} }
...@@ -34,13 +26,18 @@ class App extends PureComponent<any, any> { ...@@ -34,13 +26,18 @@ class App extends PureComponent<any, any> {
render() { render() {
return( return(
<Router> <Router>
<div> <Switch>
<Redirect from="/" to="dashboard" /> <Redirect path="/" exact to="/dashboard" />
<Route path="/dashboard" component={ Dashboard } /> <Route path="/dashboard" component={ Dashboard } />
</div> <Route component={ NotFound } />
</Switch>
</Router> </Router>
); );
} }
} }
export default connect(mapStateToProps, mapDispatchToProps)(App); const mapStateToProps = (state: any) => {
return state.authContext;
};
export default connect(mapStateToProps)(App);
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getTasksAsync } from '../../../actions/tasks.actions';
import { Layout, Empty, Button, Modal, Col, Row } from 'antd'; import { Layout, Empty, Button, Modal, Col, Row } from 'antd';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
...@@ -8,16 +11,9 @@ import './dashboard-content.scss'; ...@@ -8,16 +11,9 @@ import './dashboard-content.scss';
const { Content } = Layout; const { Content } = Layout;
const { confirm } = Modal; const { confirm } = Modal;
interface DashboardContentAction {
id: number,
name: string,
trigger: Function,
}
class DashboardContent extends Component<any, any> { class DashboardContent extends Component<any, any> {
hostUrl: string | undefined; hostUrl: string | undefined;
apiUrl: string | undefined; apiUrl: string | undefined;
actions: DashboardContentAction[];
constructor(props: any) { constructor(props: any) {
super(props); super(props);
...@@ -25,37 +21,6 @@ class DashboardContent extends Component<any, any> { ...@@ -25,37 +21,6 @@ class DashboardContent extends Component<any, any> {
this.state = {}; this.state = {};
this.hostUrl = process.env.REACT_APP_API_HOST_URL; this.hostUrl = process.env.REACT_APP_API_HOST_URL;
this.apiUrl = process.env.REACT_APP_API_FULL_URL; this.apiUrl = process.env.REACT_APP_API_FULL_URL;
this.actions = [
{
id: 1,
name: 'Dump annotation',
trigger: () => {
this.onDumpAnnotation();
},
},
{
id: 2,
name: 'Upload annotation',
trigger: () => {
this.onUploadAnnotation();
},
},
{
id: 3,
name: 'Update task',
trigger: (task: any) => {
this.onUpdateTask(task);
},
},
{
id: 4,
name: 'Delete task',
trigger: (task: any) => {
this.onDeleteTask(task);
},
},
];
} }
render() { render() {
...@@ -66,48 +31,11 @@ class DashboardContent extends Component<any, any> { ...@@ -66,48 +31,11 @@ class DashboardContent extends Component<any, any> {
); );
} }
private onDumpAnnotation() {
console.log('Dump');
}
private onUploadAnnotation() {
console.log('Upload');
}
private onUpdateTask(task: any) {
console.log('Update');
}
private onDeleteTask(task: any) {
const props = this.props;
confirm({
title: 'Do you want to delete this task?',
okText: 'Yes',
okType: 'danger',
centered: true,
onOk() {
return props.deleteTask(task);
},
cancelText: 'No',
onCancel() {
return;
},
});
}
private renderPlaceholder() { private renderPlaceholder() {
return ( return (
<Empty <Empty className="empty" description="No tasks found...">
className="empty" <Button type="primary" onClick={ this.createTask }>
description={ Create task
<span>
No tasks found...
</span>
}
>
<Button type="primary">
Create a new task
</Button> </Button>
</Empty> </Empty>
) )
...@@ -132,17 +60,26 @@ class DashboardContent extends Component<any, any> { ...@@ -132,17 +60,26 @@ class DashboardContent extends Component<any, any> {
</Col> </Col>
<Col className="card-actions" span={8}> <Col className="card-actions" span={8}>
{ <Row type="flex">
this.actions.map( <Button type="primary" onClick={ this.onDumpAnnotation }>
(action: DashboardContentAction) => ( Dump annotation
<Row type="flex" key={ action.id }> </Button>
<Button type="primary" onClick={ () => action.trigger(task) }> </Row>
{ action.name } <Row type="flex">
</Button> <Button type="primary" onClick={ this.onUploadAnnotation }>
</Row> Upload annotation
) </Button>
) </Row>
} <Row type="flex">
<Button type="primary" onClick={ this.onUpdateTask }>
Update task
</Button>
</Row>
<Row type="flex">
<Button type="primary" onClick={ () => this.onDeleteTask(task) }>
Delete task
</Button>
</Row>
</Col> </Col>
<Col className="сard-jobs" span={8}> <Col className="сard-jobs" span={8}>
...@@ -165,6 +102,49 @@ class DashboardContent extends Component<any, any> { ...@@ -165,6 +102,49 @@ class DashboardContent extends Component<any, any> {
</Content> </Content>
); );
} }
private createTask = () => {
console.log('Create task');
}
private onUpdateTask = (task: any) => {
console.log('Update task');
}
private onDeleteTask = (task: any) => {
const self = this;
confirm({
title: 'Do you want to delete this task?',
okText: 'Yes',
okType: 'danger',
centered: true,
onOk(closeFunction: Function) {
return task.delete().then(
() => {
self.props.dispatch(getTasksAsync());
closeFunction();
},
);
},
cancelText: 'No',
onCancel() {
return;
},
});
}
private onDumpAnnotation = () => {
console.log('Dump annotatio');
}
private onUploadAnnotation = () => {
console.log('Upload annotation');
}
} }
export default DashboardContent; const mapStateToProps = (state: any) => {
return state.tasks;
};
export default connect(mapStateToProps)(DashboardContent);
import React, { Component } from 'react'; import React, { PureComponent } from 'react';
import { Location, Action } from 'history';
import * as queryString from 'query-string';
import { connect } from 'react-redux';
import { getTasksAsync } from '../../actions/tasks.actions';
import { filterTasks } from '../../actions/tasks-filter.actions';
import { Layout } from 'antd'; import { Layout } from 'antd';
...@@ -8,77 +15,50 @@ import DashboardFooter from './footer/dashboard-footer'; ...@@ -8,77 +15,50 @@ import DashboardFooter from './footer/dashboard-footer';
import './dashboard.scss'; import './dashboard.scss';
interface DashboardState { class Dashboard extends PureComponent<any, any> {
tasks: [];
}
class Dashboard extends Component<any, DashboardState> {
constructor(props: any) {
super(props);
this.state = { tasks: [] };
}
componentDidMount() { componentDidMount() {
this.getTasks(); this.loadTasks(this.props.location.search);
this.props.history.listen((location: Location, action: Action) => {
this.loadTasks(location.search);
})
} }
render() { render() {
return ( return (
<Layout className="layout"> <Layout className="layout">
<DashboardHeader <DashboardHeader />
onSearch={ this.getTasks }> <DashboardContent />
</DashboardHeader> <DashboardFooter />
<DashboardContent
tasks={ this.state.tasks }
deleteTask={ this.deleteTask }>
</DashboardContent>
<DashboardFooter
tasksCount={ (this.state.tasks as any)['count'] }
onPageChange={ this.onPageChange }>
</DashboardFooter>
</Layout> </Layout>
); );
} }
private getTasks = (query?: string) => { private loadTasks = (params: any) => {
const queryObject = { const query = queryString.parse(params);
search: query const queryObject = this.setQueryObject(query);
};
(window as any).cvat.tasks.get(query ? queryObject : {}).then(
(tasks: any) => {
this.setState({ tasks });
},
(error: any) => {
console.log(error);
}
);
}
private onPageChange = (page: number) => { this.props.dispatch(filterTasks(queryObject));
(window as any).cvat.tasks.get({ page }).then( this.props.dispatch(getTasksAsync(queryObject));
(tasks: any) => {
this.setState({ tasks });
},
(error: any) => {
console.log(error);
}
);
} }
private deleteTask = (task: any) => { private setQueryObject = (params: { search?: string, page?: string }): { search?: string, page?: number } => {
task.delete().then( const queryObject: { search?: string, page?: number } = {};
(_deleted: any) => {
this.getTasks(); if (params['search']) {
}, queryObject.search = params.search.toString();
(error: any) => { }
console.log(error);
} if (params['page']) {
); queryObject.page = parseInt(params.page);
}
return queryObject;
} }
} }
export default Dashboard; const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default connect(mapStateToProps)(Dashboard);
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import * as queryString from 'query-string';
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux';
import { Layout, Pagination, Row, Col } from 'antd'; import { Layout, Pagination, Row, Col } from 'antd';
import './dashboard-footer.scss'; import './dashboard-footer.scss';
...@@ -8,25 +14,32 @@ const { Footer } = Layout; ...@@ -8,25 +14,32 @@ const { Footer } = Layout;
class DashboardFooter extends PureComponent<any, any> { class DashboardFooter extends PureComponent<any, any> {
render() { render() {
const pagination = (
<Col span={24}>
<Pagination
className="dashboard-footer__pagination"
hideOnSinglePage
onChange={ this.props.onPageChange }
total={ this.props.tasksCount }>
</Pagination>
</Col>
);
return( return(
<Footer className="dashboard-footer"> <Footer className="dashboard-footer">
<Row type="flex" gutter={16}> <Row type="flex" gutter={16}>
{ this.props.tasksCount ? pagination : '' } <Col span={24}>
<Pagination
className="dashboard-footer__pagination"
current={ this.props.currentPage || 1 }
hideOnSinglePage
onChange={ this.onPageChange }
total={ this.props.tasksCount }>
</Pagination>
</Col>
</Row> </Row>
</Footer> </Footer>
); );
} }
private onPageChange = (page: number, pageSize?: number) => {
const params = { search: this.props.searchQuery, page }
this.props.history.push({ search: queryString.stringify(params) });
}
} }
export default DashboardFooter; const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default withRouter(connect(mapStateToProps)(DashboardFooter) as any);
import React, { Component } from 'react'; import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { Layout, Row, Col, Button, Input } from 'antd'; import { Layout, Row, Col, Button, Input } from 'antd';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
...@@ -8,35 +12,21 @@ import './dashboard-header.scss'; ...@@ -8,35 +12,21 @@ import './dashboard-header.scss';
const { Header } = Layout; const { Header } = Layout;
const { Search } = Input; const { Search } = Input;
interface DashboardHeaderAction {
id: number,
name: string,
trigger: Function,
}
class DashboardHeader extends Component<any, any> { class DashboardHeader extends Component<any, any> {
actions: DashboardHeaderAction[];
hostUrl: string | undefined; hostUrl: string | undefined;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = {}; this.state = { searchQuery: this.props.searchQuery };
this.hostUrl = process.env.REACT_APP_API_HOST_URL; this.hostUrl = process.env.REACT_APP_API_HOST_URL;
}
this.actions = [ componentDidUpdate(prevProps: any) {
{ if (this.props.searchQuery !== prevProps.searchQuery) {
id: 1, this.setState({ searchQuery: this.props.searchQuery });
name: 'Create task', }
trigger: () => {},
},
{
id: 2,
name: 'User guide',
trigger: this.openUserGuide,
},
];
} }
render() { render() {
...@@ -50,33 +40,49 @@ class DashboardHeader extends Component<any, any> { ...@@ -50,33 +40,49 @@ class DashboardHeader extends Component<any, any> {
<Search <Search
className="search" className="search"
placeholder="Search for tasks" placeholder="Search for tasks"
onSearch={ query => this.props.onSearch(query) } value={ this.state.searchQuery }
onChange={ this.onValueChange }
onSearch={ query => this.onSearch(query) }
enterButton> enterButton>
</Search> </Search>
</Col> </Col>
<Col className="dashboard-header__actions" span={8}> <Col className="dashboard-header__actions" span={8}>
{ <Button
this.actions.map( className="action"
(action: DashboardHeaderAction) => ( type="primary"
<Button onClick={ this.createTask }>
className="action" Create task
type="primary" </Button>
key={ action.id } <Button
onClick={ () => action.trigger() }> className="action"
{ action.name } type="primary"
</Button> href={ `${this.hostUrl}/documentation/user_guide.html` }
) target="blank">
) User guide
} </Button>
</Col> </Col>
</Row> </Row>
</Header> </Header>
); );
} }
private openUserGuide = () => { private createTask = () => {
window.open(`${this.hostUrl}/documentation/user_guide.html`, '_blank') console.log('Create task');
}
private onValueChange = (event: any) => {
this.setState({ searchQuery: event.target.value });
}
private onSearch = (query: string) => {
if (query !== this.props.searchQuery) {
query ? this.props.history.push(`?search=${query}`) : this.props.history.push(this.props.location.pathname);
}
} }
} }
export default DashboardHeader; const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default withRouter(connect(mapStateToProps)(DashboardHeader) as any);
.not-found {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
}
import React from 'react';
import ReactDOM from 'react-dom';
import NotFound from './not-found';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<NotFound />, div);
ReactDOM.unmountComponentAtNode(div);
});
import React, { PureComponent } from 'react';
import { Empty, Button } from 'antd';
import './not-found.scss';
class NotFound extends PureComponent<any, any> {
render() {
return(
<Empty className="not-found" description="Page not found...">
<Button type="primary" href="/dashboard">
Go back to dashboard
</Button>
</Empty>
);
}
}
export default NotFound;
export default (state = {}, action: any) => { export default (state = {}, action: any) => {
switch (action.type) { switch (action.type) {
case 'LOGIN': case 'LOGIN':
return { ...state, isLoggedIn: action.payload }; return { ...state, isAuthenticated: action.payload };
case 'LOGOUT': case 'LOGOUT':
return { ...state, isLoggedIn: action.payload }; return { ...state, isAuthenticated: action.payload };
default: default:
return state; return state;
} }
......
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import authenticationReducer from './authenticate-reducer'; import authContext from './auth.reducer';
import tasks from './tasks.reducer';
import tasksFilter from './tasks-filter.reducer';
export default combineReducers({ export default combineReducers({
authenticationReducer, authContext,
tasks,
tasksFilter,
}); });
export default (state = { searchQuery: '', currentPage: 1 }, action: any) => {
switch (action.type) {
case 'FILTER_TASKS':
return {
...state,
searchQuery: action.payload.search,
currentPage: action.payload.page,
};
default:
return state;
}
}
export default (state: any = { tasks: [], tasksCount: 0 }, action: any) => {
switch (action.type) {
case 'GET_TASKS':
return {
...state,
tasks: Array.from(action.payload.values()),
tasksCount: action.payload.count,
};
default:
return state;
}
}
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import rootReducer from './reducers/root-reducer'; import rootReducer from './reducers/root.reducer';
export default function configureStore(initialState = {}) { export default function configureStore(initialState = {}) {
return createStore( return createStore(
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册