mirror of
https://github.com/opensupports/opensupports.git
synced 2025-07-31 01:35:15 +02:00
Ivan - Add all ticket search and server pagination [skip ci]
This commit is contained in:
parent
cc1b5785fa
commit
89837b0726
@ -32,12 +32,22 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
retrieveAllTickets() {
|
retrieveAllTickets(page) {
|
||||||
return {
|
return {
|
||||||
type: 'ALL_TICKETS',
|
type: 'ALL_TICKETS',
|
||||||
payload: API.call({
|
payload: API.call({
|
||||||
path: '/staff/get-all-tickets',
|
path: '/staff/get-all-tickets',
|
||||||
data: {}
|
data: {page}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
searchTickets(query, page) {
|
||||||
|
return {
|
||||||
|
type: 'ALL_TICKETS',
|
||||||
|
payload: API.call({
|
||||||
|
path: '/staff/search-tickets',
|
||||||
|
data: {query, page}
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,7 @@ class TicketList extends React.Component {
|
|||||||
departments: React.PropTypes.array,
|
departments: React.PropTypes.array,
|
||||||
loading: React.PropTypes.bool,
|
loading: React.PropTypes.bool,
|
||||||
ticketPath: React.PropTypes.string,
|
ticketPath: React.PropTypes.string,
|
||||||
|
showDepartmentDropdown: React.PropTypes.bool,
|
||||||
tickets: React.PropTypes.arrayOf(React.PropTypes.object),
|
tickets: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
type: React.PropTypes.oneOf([
|
type: React.PropTypes.oneOf([
|
||||||
'primary',
|
'primary',
|
||||||
@ -23,6 +24,7 @@ class TicketList extends React.Component {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
showDepartmentDropdown: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
tickets: [],
|
tickets: [],
|
||||||
departments: [],
|
departments: [],
|
||||||
@ -37,8 +39,8 @@ class TicketList extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className="ticket-list">
|
<div className="ticket-list">
|
||||||
{(this.props.type === 'secondary') ? this.renderDepartmentsDropDown() : null}
|
{(this.props.type === 'secondary' && this.props.showDepartmentDropdown) ? this.renderDepartmentsDropDown() : null}
|
||||||
<Table loading={this.props.loading} headers={this.getTableHeaders()} rows={this.getTableRows()} pageSize={10} comp={this.compareFunction} />
|
<Table {...this.getTableProps()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -62,6 +64,19 @@ class TicketList extends React.Component {
|
|||||||
size: 'medium'
|
size: 'medium'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getTableProps() {
|
||||||
|
return {
|
||||||
|
loading: this.props.loading,
|
||||||
|
headers: this.getTableHeaders(),
|
||||||
|
rows: this.getTableRows(),
|
||||||
|
pageSize: 10,
|
||||||
|
comp: this.compareFunction,
|
||||||
|
page: this.props.page,
|
||||||
|
pages: this.props.pages,
|
||||||
|
onPageChange: this.props.onPageChange
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
getDepartments() {
|
getDepartments() {
|
||||||
let departments = this.props.departments.map((department) => {
|
let departments = this.props.departments.map((department) => {
|
||||||
|
@ -6,6 +6,7 @@ import i18n from 'lib-app/i18n';
|
|||||||
import AdminDataAction from 'actions/admin-data-actions';
|
import AdminDataAction from 'actions/admin-data-actions';
|
||||||
import Header from 'core-components/header';
|
import Header from 'core-components/header';
|
||||||
import TicketList from 'app-components/ticket-list';
|
import TicketList from 'app-components/ticket-list';
|
||||||
|
import SearchBox from 'core-components/search-box';
|
||||||
|
|
||||||
class AdminPanelAllTickets extends React.Component {
|
class AdminPanelAllTickets extends React.Component {
|
||||||
|
|
||||||
@ -14,6 +15,11 @@ class AdminPanelAllTickets extends React.Component {
|
|||||||
tickets: []
|
tickets: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
page: 1,
|
||||||
|
query: ''
|
||||||
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.dispatch(AdminDataAction.retrieveAllTickets());
|
this.props.dispatch(AdminDataAction.retrieveAllTickets());
|
||||||
}
|
}
|
||||||
@ -22,26 +28,51 @@ class AdminPanelAllTickets extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="admin-panel-my-tickets">
|
<div className="admin-panel-my-tickets">
|
||||||
<Header title={i18n('ALL_TICKETS')} description={i18n('ALL_TICKETS_DESCRIPTION')} />
|
<Header title={i18n('ALL_TICKETS')} description={i18n('ALL_TICKETS_DESCRIPTION')} />
|
||||||
<TicketList {...this.getProps()}/>
|
<div className="admin-panel-my-tickets__search-box">
|
||||||
|
<SearchBox onSearch={this.onSearch.bind(this)} />
|
||||||
|
</div>
|
||||||
|
<TicketList {...this.getTicketListProps()}/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProps() {
|
getTicketListProps() {
|
||||||
return {
|
return {
|
||||||
|
showDepartmentDropdown: false,
|
||||||
departments: this.props.departments,
|
departments: this.props.departments,
|
||||||
tickets: this.props.tickets,
|
tickets: this.props.tickets,
|
||||||
type: 'secondary',
|
type: 'secondary',
|
||||||
loading: this.props.loading,
|
loading: this.props.loading,
|
||||||
ticketPath: '/admin/panel/tickets/view-ticket/'
|
ticketPath: '/admin/panel/tickets/view-ticket/',
|
||||||
|
onPageChange: this.onPageChange.bind(this),
|
||||||
|
page: this.state.page,
|
||||||
|
pages: this.props.pages
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearch(query) {
|
||||||
|
this.setState({query, page: 1});
|
||||||
|
this.props.dispatch(AdminDataAction.searchTickets(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(event) {
|
||||||
|
this.setState({
|
||||||
|
page: event.target.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if(this.state.query) {
|
||||||
|
this.props.dispatch(AdminDataAction.searchTickets(this.state.query, event.target.value));
|
||||||
|
} else {
|
||||||
|
this.props.dispatch(AdminDataAction.retrieveAllTickets(event.target.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect((store) => {
|
export default connect((store) => {
|
||||||
return {
|
return {
|
||||||
departments: store.session.userDepartments,
|
departments: store.session.userDepartments,
|
||||||
tickets: store.adminData.allTickets,
|
tickets: store.adminData.allTickets,
|
||||||
|
pages: store.adminData.allTicketsPages,
|
||||||
loading: !store.adminData.allTicketsLoaded
|
loading: !store.adminData.allTicketsLoaded
|
||||||
};
|
};
|
||||||
})(AdminPanelAllTickets);
|
})(AdminPanelAllTickets);
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
.admin-panel-my-tickets {
|
||||||
|
&__search-box {
|
||||||
|
padding: 0 50px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
41
client/src/core-components/search-box.js
Normal file
41
client/src/core-components/search-box.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Input from 'core-components/input';
|
||||||
|
import Icon from 'core-components/icon';
|
||||||
|
import keyCode from 'keycode';
|
||||||
|
|
||||||
|
class SearchBox extends React.Component {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
onSearch: React.PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
value: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="search-box">
|
||||||
|
<Input className="search-box__text" value={this.state.value} onChange={this.onChange.bind(this)} onKeyDown={this.onKeyDown.bind(this)} />
|
||||||
|
<span className="search-box__icon">
|
||||||
|
<Icon name="search" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(event) {
|
||||||
|
this.setState({
|
||||||
|
value: event.target.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown(event) {
|
||||||
|
if(keyCode(event) === 'enter' && this.props.onSearch) {
|
||||||
|
this.props.onSearch(this.state.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBox;
|
21
client/src/core-components/search-box.scss
Normal file
21
client/src/core-components/search-box.scss
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
@import "../scss/vars";
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
color: $dark-grey;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
width: 100%;
|
||||||
|
font-size: $font-size--lg;
|
||||||
|
|
||||||
|
.input__text {
|
||||||
|
padding-left: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 20px;
|
||||||
|
}
|
||||||
|
}
|
@ -13,9 +13,12 @@ class Table extends React.Component {
|
|||||||
className: React.PropTypes.string
|
className: React.PropTypes.string
|
||||||
})),
|
})),
|
||||||
rows: React.PropTypes.arrayOf(React.PropTypes.object),
|
rows: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||||
pageSize: React.PropTypes.number,
|
|
||||||
loading: React.PropTypes.bool,
|
loading: React.PropTypes.bool,
|
||||||
type: React.PropTypes.oneOf(['default']),
|
type: React.PropTypes.oneOf(['default']),
|
||||||
|
page: React.PropTypes.number,
|
||||||
|
pages: React.PropTypes.number,
|
||||||
|
pageSize: React.PropTypes.number,
|
||||||
|
onPageChange: React.PropTypes.func,
|
||||||
comp: React.PropTypes.func
|
comp: React.PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,7 +44,7 @@ class Table extends React.Component {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{(this.props.loading) ? this.renderLoading() : null}
|
{(this.props.loading) ? this.renderLoading() : null}
|
||||||
{(this.props.pageSize && this.props.rows.length > this.props.pageSize) ? this.renderNavigation() : null}
|
{this.renderPagination()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -59,8 +62,8 @@ class Table extends React.Component {
|
|||||||
|
|
||||||
renderRow(row, index) {
|
renderRow(row, index) {
|
||||||
const headersKeys = this.props.headers.map(header => header.key);
|
const headersKeys = this.props.headers.map(header => header.key);
|
||||||
const minIndex = this.props.pageSize * (this.state.page - 1);
|
const minIndex = this.props.pageSize * ((this.props.page) ? 0 : this.state.page - 1);
|
||||||
const maxIndex = this.props.pageSize * this.state.page;
|
const maxIndex = this.props.pageSize * ((this.props.page) ? 1 : this.state.page);
|
||||||
const shouldRenderRow = !this.props.pageSize || (index >= minIndex && index < maxIndex);
|
const shouldRenderRow = !this.props.pageSize || (index >= minIndex && index < maxIndex);
|
||||||
|
|
||||||
return (shouldRenderRow) ? (
|
return (shouldRenderRow) ? (
|
||||||
@ -81,12 +84,15 @@ class Table extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderPagination() {
|
||||||
|
return (this.props.pages || (this.props.pageSize && this.props.rows.length > this.props.pageSize)) ? this.renderNavigation() : null
|
||||||
|
}
|
||||||
|
|
||||||
renderNavigation() {
|
renderNavigation() {
|
||||||
const pages = Math.ceil(this.props.rows.length / this.props.pageSize) + 1;
|
const items = _.range(1, this.getPages()).map((index) => {return {content: index};});
|
||||||
const items = _.range(1, pages).map((index) => {return {content: index};});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu className="table__navigation" type="navigation" items={items} onItemClick={this.onNavigationItemClick.bind(this)}/>
|
<Menu className="table__navigation" type="navigation" items={items} selectedIndex={this.getPageNumber() - 1} onItemClick={this.onNavigationItemClick.bind(this)} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +108,10 @@ class Table extends React.Component {
|
|||||||
this.setState({
|
this.setState({
|
||||||
page: index + 1
|
page: index + 1
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if(this.props.onPageChange) {
|
||||||
|
this.props.onPageChange({target: {value: index + 1}});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRowClass(row) {
|
getRowClass(row) {
|
||||||
@ -114,11 +124,19 @@ class Table extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRows() {
|
getRows() {
|
||||||
let v = _.clone(this.props.rows);
|
let sortedRows = _.clone(this.props.rows);
|
||||||
v.sort(this.props.comp);
|
sortedRows.sort(this.props.comp);
|
||||||
return v;
|
|
||||||
|
return sortedRows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPages() {
|
||||||
|
return (this.props.pages !== undefined) ? this.props.pages + 1 : Math.ceil(this.props.rows.length / this.props.pageSize) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageNumber() {
|
||||||
|
return (this.props.page !== undefined) ? this.props.page: this.state.page;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Table;
|
export default Table;
|
@ -50,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__loading-wrapper {
|
&__loading-wrapper {
|
||||||
min-height: 200px;
|
min-height: 380px;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: $grey;
|
background-color: $grey;
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
module.exports = [
|
module.exports = [
|
||||||
{
|
{
|
||||||
path: '/staff/get',
|
path: '/staff/get',
|
||||||
@ -498,90 +500,81 @@ module.exports = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/staff/get-all-tickets',
|
path: '/staff/get-all-tickets',
|
||||||
|
time: 1000,
|
||||||
|
response: function () {
|
||||||
|
return {
|
||||||
|
status: 'success',
|
||||||
|
data: {
|
||||||
|
tickets: _.range(0, 10).map(() => {
|
||||||
|
return {
|
||||||
|
ticketNumber: '445441',
|
||||||
|
title: 'Inscription ACM ICPC',
|
||||||
|
content: 'I had a problem with the installation of the php server',
|
||||||
|
department: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Sales Support'
|
||||||
|
},
|
||||||
|
date: '20160416',
|
||||||
|
file: 'http://www.opensupports.com/some_file.zip',
|
||||||
|
language: 'en',
|
||||||
|
unread: false,
|
||||||
|
closed: false,
|
||||||
|
priority: 'low',
|
||||||
|
author: {
|
||||||
|
id: 12,
|
||||||
|
name: 'Haskell Curry',
|
||||||
|
email: 'haskell@lambda.com'
|
||||||
|
},
|
||||||
|
owner: {
|
||||||
|
id: 15,
|
||||||
|
name: 'Steve Jobs',
|
||||||
|
email: 'steve@jobs.com'
|
||||||
|
},
|
||||||
|
events: []
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
pages: 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/staff/search-tickets',
|
||||||
time: 300,
|
time: 300,
|
||||||
response: function () {
|
response: function () {
|
||||||
return {
|
return {
|
||||||
status: 'success',
|
status: 'success',
|
||||||
data: [
|
data: {
|
||||||
{
|
tickets: _.range(0, 10).map(() => {
|
||||||
ticketNumber: '445441',
|
return {
|
||||||
title: 'Inscription ACM ICPC',
|
ticketNumber: '445441',
|
||||||
content: 'I had a problem with the installation of the php server',
|
title: 'Inscription ACM ICPC',
|
||||||
department: {
|
content: 'I had a problem with the installation of the php server',
|
||||||
id: 1,
|
department: {
|
||||||
name: 'Sales Support'
|
id: 1,
|
||||||
},
|
name: 'Sales Support'
|
||||||
date: '20160416',
|
},
|
||||||
file: 'http://www.opensupports.com/some_file.zip',
|
date: '20160416',
|
||||||
language: 'en',
|
file: 'http://www.opensupports.com/some_file.zip',
|
||||||
unread: true,
|
language: 'en',
|
||||||
closed: false,
|
unread: false,
|
||||||
priority: 'low',
|
closed: false,
|
||||||
author: {
|
priority: 'low',
|
||||||
id: 12,
|
author: {
|
||||||
name: 'Haskell Curry',
|
id: 12,
|
||||||
email: 'haskell@lambda.com'
|
name: 'Haskell Curry',
|
||||||
},
|
email: 'haskell@lambda.com'
|
||||||
owner: {
|
},
|
||||||
id: 15,
|
owner: {
|
||||||
name: 'Steve Jobs',
|
id: 15,
|
||||||
email: 'steve@jobs.com'
|
name: 'Steve Jobs',
|
||||||
},
|
email: 'steve@jobs.com'
|
||||||
events: []
|
},
|
||||||
},
|
events: []
|
||||||
{
|
};
|
||||||
ticketNumber: '445441',
|
}),
|
||||||
title: 'Inscription ACM ICPC',
|
pages: 2
|
||||||
content: 'I had a problem with the installation of the php server',
|
}
|
||||||
department: {
|
|
||||||
id: 1,
|
|
||||||
name: 'Sales Support'
|
|
||||||
},
|
|
||||||
date: '20160416',
|
|
||||||
file: 'http://www.opensupports.com/some_file.zip',
|
|
||||||
language: 'en',
|
|
||||||
unread: true,
|
|
||||||
closed: false,
|
|
||||||
priority: 'low',
|
|
||||||
author: {
|
|
||||||
id: 12,
|
|
||||||
name: 'Haskell Curry',
|
|
||||||
email: 'haskell@lambda.com'
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
id: 15,
|
|
||||||
name: 'Steve Jobs',
|
|
||||||
email: 'steve@jobs.com'
|
|
||||||
},
|
|
||||||
events: []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ticketNumber: '445441',
|
|
||||||
title: 'Code jam is awesome',
|
|
||||||
content: 'I had a problem with the installation of the php server',
|
|
||||||
department: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Technical Issues'
|
|
||||||
},
|
|
||||||
date: '20160416',
|
|
||||||
file: 'http://www.opensupports.com/some_file.zip',
|
|
||||||
language: 'en',
|
|
||||||
unread: true,
|
|
||||||
closed: false,
|
|
||||||
priority: 'low',
|
|
||||||
author: {
|
|
||||||
id: 12,
|
|
||||||
name: 'Haskell Curry',
|
|
||||||
email: 'haskell@lambda.com'
|
|
||||||
},
|
|
||||||
owner: {
|
|
||||||
id: 15,
|
|
||||||
name: 'Steve Jobs',
|
|
||||||
email: 'steve@jobs.com'
|
|
||||||
},
|
|
||||||
events: []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,8 @@ class AdminDataReducer extends Reducer {
|
|||||||
|
|
||||||
onAllTicketsRetrieved(state, payload) {
|
onAllTicketsRetrieved(state, payload) {
|
||||||
return _.extend({}, state, {
|
return _.extend({}, state, {
|
||||||
allTickets: payload.data,
|
allTickets: payload.data.tickets,
|
||||||
|
allTicketsPages: payload.data.pages,
|
||||||
allTicketsLoaded: true
|
allTicketsLoaded: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user