提交 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 @@
"less": "^3.9.0",
"less-loader": "^5.0.0",
"node-sass": "^4.12.0",
"query-string": "^6.8.1",
"react": "^16.8.6",
"react-app-rewired": "^2.1.3",
"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 { 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 { login, logout } from '../../actions/auth.actions';
import Dashboard from '../dashboard/dashboard';
import { loginAction, logoutAction } from '../../actions/authentication-action';
import NotFound from '../not-found/not-found';
import './app.scss';
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> {
componentDidMount() {
window.cvat.server.login(process.env.REACT_APP_LOGIN, process.env.REACT_APP_PASSWORD).then(
(_response: any) => {
this.props.login();
this.props.dispatch(login(true));
},
(_error: any) => {
this.props.logout();
this.props.dispatch(logout(false));
}
);
}
......@@ -34,13 +26,18 @@ class App extends PureComponent<any, any> {
render() {
return(
<Router>
<div>
<Redirect from="/" to="dashboard" />
<Switch>
<Redirect path="/" exact to="/dashboard" />
<Route path="/dashboard" component={ Dashboard } />
</div>
<Route component={ NotFound } />
</Switch>
</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 { connect } from 'react-redux';
import { getTasksAsync } from '../../../actions/tasks.actions';
import { Layout, Empty, Button, Modal, Col, Row } from 'antd';
import Title from 'antd/lib/typography/Title';
......@@ -8,16 +11,9 @@ import './dashboard-content.scss';
const { Content } = Layout;
const { confirm } = Modal;
interface DashboardContentAction {
id: number,
name: string,
trigger: Function,
}
class DashboardContent extends Component<any, any> {
hostUrl: string | undefined;
apiUrl: string | undefined;
actions: DashboardContentAction[];
constructor(props: any) {
super(props);
......@@ -25,37 +21,6 @@ class DashboardContent extends Component<any, any> {
this.state = {};
this.hostUrl = process.env.REACT_APP_API_HOST_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() {
......@@ -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() {
return (
<Empty
className="empty"
description={
<span>
No tasks found...
</span>
}
>
<Button type="primary">
Create a new task
<Empty className="empty" description="No tasks found...">
<Button type="primary" onClick={ this.createTask }>
Create task
</Button>
</Empty>
)
......@@ -132,17 +60,26 @@ class DashboardContent extends Component<any, any> {
</Col>
<Col className="card-actions" span={8}>
{
this.actions.map(
(action: DashboardContentAction) => (
<Row type="flex" key={ action.id }>
<Button type="primary" onClick={ () => action.trigger(task) }>
{ action.name }
</Button>
</Row>
)
)
}
<Row type="flex">
<Button type="primary" onClick={ this.onDumpAnnotation }>
Dump annotation
</Button>
</Row>
<Row type="flex">
<Button type="primary" onClick={ this.onUploadAnnotation }>
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 className="сard-jobs" span={8}>
......@@ -165,6 +102,49 @@ class DashboardContent extends Component<any, any> {
</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';
......@@ -8,77 +15,50 @@ import DashboardFooter from './footer/dashboard-footer';
import './dashboard.scss';
interface DashboardState {
tasks: [];
}
class Dashboard extends Component<any, DashboardState> {
constructor(props: any) {
super(props);
this.state = { tasks: [] };
}
class Dashboard extends PureComponent<any, any> {
componentDidMount() {
this.getTasks();
this.loadTasks(this.props.location.search);
this.props.history.listen((location: Location, action: Action) => {
this.loadTasks(location.search);
})
}
render() {
return (
<Layout className="layout">
<DashboardHeader
onSearch={ this.getTasks }>
</DashboardHeader>
<DashboardContent
tasks={ this.state.tasks }
deleteTask={ this.deleteTask }>
</DashboardContent>
<DashboardFooter
tasksCount={ (this.state.tasks as any)['count'] }
onPageChange={ this.onPageChange }>
</DashboardFooter>
<DashboardHeader />
<DashboardContent />
<DashboardFooter />
</Layout>
);
}
private getTasks = (query?: string) => {
const queryObject = {
search: query
};
(window as any).cvat.tasks.get(query ? queryObject : {}).then(
(tasks: any) => {
this.setState({ tasks });
},
(error: any) => {
console.log(error);
}
);
}
private loadTasks = (params: any) => {
const query = queryString.parse(params);
const queryObject = this.setQueryObject(query);
private onPageChange = (page: number) => {
(window as any).cvat.tasks.get({ page }).then(
(tasks: any) => {
this.setState({ tasks });
},
(error: any) => {
console.log(error);
}
);
this.props.dispatch(filterTasks(queryObject));
this.props.dispatch(getTasksAsync(queryObject));
}
private deleteTask = (task: any) => {
task.delete().then(
(_deleted: any) => {
this.getTasks();
},
(error: any) => {
console.log(error);
}
);
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;
}
}
export default Dashboard;
const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default connect(mapStateToProps)(Dashboard);
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 './dashboard-footer.scss';
......@@ -8,25 +14,32 @@ const { Footer } = Layout;
class DashboardFooter extends PureComponent<any, any> {
render() {
const pagination = (
<Col span={24}>
<Pagination
className="dashboard-footer__pagination"
hideOnSinglePage
onChange={ this.props.onPageChange }
total={ this.props.tasksCount }>
</Pagination>
</Col>
);
return(
<Footer className="dashboard-footer">
<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>
</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 { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { Layout, Row, Col, Button, Input } from 'antd';
import Title from 'antd/lib/typography/Title';
......@@ -8,35 +12,21 @@ import './dashboard-header.scss';
const { Header } = Layout;
const { Search } = Input;
interface DashboardHeaderAction {
id: number,
name: string,
trigger: Function,
}
class DashboardHeader extends Component<any, any> {
actions: DashboardHeaderAction[];
hostUrl: string | undefined;
constructor(props: any) {
super(props);
this.state = {};
this.state = { searchQuery: this.props.searchQuery };
this.hostUrl = process.env.REACT_APP_API_HOST_URL;
}
this.actions = [
{
id: 1,
name: 'Create task',
trigger: () => {},
},
{
id: 2,
name: 'User guide',
trigger: this.openUserGuide,
},
];
componentDidUpdate(prevProps: any) {
if (this.props.searchQuery !== prevProps.searchQuery) {
this.setState({ searchQuery: this.props.searchQuery });
}
}
render() {
......@@ -50,33 +40,49 @@ class DashboardHeader extends Component<any, any> {
<Search
className="search"
placeholder="Search for tasks"
onSearch={ query => this.props.onSearch(query) }
value={ this.state.searchQuery }
onChange={ this.onValueChange }
onSearch={ query => this.onSearch(query) }
enterButton>
</Search>
</Col>
<Col className="dashboard-header__actions" span={8}>
{
this.actions.map(
(action: DashboardHeaderAction) => (
<Button
className="action"
type="primary"
key={ action.id }
onClick={ () => action.trigger() }>
{ action.name }
</Button>
)
)
}
<Button
className="action"
type="primary"
onClick={ this.createTask }>
Create task
</Button>
<Button
className="action"
type="primary"
href={ `${this.hostUrl}/documentation/user_guide.html` }
target="blank">
User guide
</Button>
</Col>
</Row>
</Header>
);
}
private openUserGuide = () => {
window.open(`${this.hostUrl}/documentation/user_guide.html`, '_blank')
private createTask = () => {
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) => {
switch (action.type) {
case 'LOGIN':
return { ...state, isLoggedIn: action.payload };
return { ...state, isAuthenticated: action.payload };
case 'LOGOUT':
return { ...state, isLoggedIn: action.payload };
return { ...state, isAuthenticated: action.payload };
default:
return state;
}
......
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({
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 thunk from 'redux-thunk';
import rootReducer from './reducers/root-reducer';
import rootReducer from './reducers/root.reducer';
export default function configureStore(initialState = {}) {
return createStore(
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册