diff --git a/client/src/app-components/activity-row.js b/client/src/app-components/activity-row.js
index cb5dec18..1b3da113 100644
--- a/client/src/app-components/activity-row.js
+++ b/client/src/app-components/activity-row.js
@@ -23,6 +23,7 @@ class ActivityRow extends React.Component {
'EDIT_SETTINGS',
'SIGNUP',
+ 'INVITE',
'ADD_TOPIC',
'ADD_ARTICLE',
'DELETE_TOPIC',
@@ -65,9 +66,7 @@ class ActivityRow extends React.Component {
-
- {this.props.author.name}
-
+ {this.renderAuthorName()}
{i18n('ACTIVITY_' + this.props.type)}
{_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : this.props.to}
@@ -76,6 +75,18 @@ class ActivityRow extends React.Component {
);
}
+ renderAuthorName() {
+ let name = this.props.author.name;
+
+ if (this.props.author.id) {
+ name =
+ {this.props.author.name}
+ ;
+ }
+
+ return name;
+ }
+
renderTicketNumber() {
let ticketNumber = (this.props.mode === 'staff') ? this.props.ticketNumber : this.props.to;
@@ -106,6 +117,7 @@ class ActivityRow extends React.Component {
'EDIT_SETTINGS': 'wrench',
'SIGNUP': 'user-plus',
+ 'INVITE': 'user-plus',
'ADD_TOPIC': 'book',
'ADD_ARTICLE': 'book',
'DELETE_TOPIC': 'book',
diff --git a/client/src/app-components/ticket-list.js b/client/src/app-components/ticket-list.js
index d23ecf0f..6f85dc43 100644
--- a/client/src/app-components/ticket-list.js
+++ b/client/src/app-components/ticket-list.js
@@ -274,4 +274,4 @@ export default connect((store) => {
return {
tags: store.config['tags']
};
-})(TicketList);
\ No newline at end of file
+})(TicketList);
diff --git a/client/src/app-components/ticket-query-list.js b/client/src/app-components/ticket-query-list.js
new file mode 100644
index 00000000..f28dbc3b
--- /dev/null
+++ b/client/src/app-components/ticket-query-list.js
@@ -0,0 +1,90 @@
+import React from 'react';
+import _ from 'lodash';
+
+import API from 'lib-app/api-call';
+import i18n from 'lib-app/i18n';
+import {connect} from 'react-redux';
+
+import TicketList from 'app-components/ticket-list';
+import Message from 'core-components/message';
+
+class TicketQueryList extends React.Component {
+
+ state = {
+ tickets: [],
+ page: 1,
+ pages: 0,
+ error: null,
+ loading: true
+ };
+
+ componentDidMount() {
+ this.getTickets();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.props.customList.title !== prevProps.customList.title) {
+ this.getTickets();
+ }
+ }
+
+ render() {
+ return (
+
+ {(this.state.error) ? {i18n('ERROR_RETRIEVING_TICKETS')} : }
+
+ );
+ }
+
+ getTickets() {
+ this.setState({
+ loading:true
+ })
+ API.call({
+ path: '/ticket/search',
+ data: {
+ page : this.state.page,
+ ...this.props.customList.filters
+ }
+ }).then((result) => {
+ this.setState({
+ tickets: result.data.tickets,
+ page: result.data.page,
+ pages: result.data.pages,
+ error: null,
+ loading: false
+ })
+ }).catch((result) => this.setState({
+ loading: false,
+ error: result.message
+ }));
+
+ }
+
+ onPageChange(event) {
+ this.setState({page: event.target.value}, () => this.getTickets());
+ }
+
+ getTicketListProps () {
+ const {page,pages,loading,tickets} = this.state;
+ return {
+ userId: this.props.userId,
+ ticketPath: '/admin/panel/tickets/view-ticket/',
+ tickets,
+ page,
+ pages,
+ loading,
+ type: 'secondary',
+ showDepartmentDropdown: false,
+ closedTicketsShown: false,
+ onPageChange:this.onPageChange.bind(this)
+ };
+ }
+
+}
+
+export default connect((store) => {
+ return {
+ userId: store.session.userId
+ };
+})(TicketQueryList);
diff --git a/client/src/app/Routes.js b/client/src/app/Routes.js
index 0419c0db..b9d43065 100644
--- a/client/src/app/Routes.js
+++ b/client/src/app/Routes.js
@@ -33,6 +33,7 @@ import AdminPanelMyAccount from 'app/admin/panel/dashboard/admin-panel-my-accoun
import AdminPanelMyTickets from 'app/admin/panel/tickets/admin-panel-my-tickets';
import AdminPanelNewTickets from 'app/admin/panel/tickets/admin-panel-new-tickets';
import AdminPanelAllTickets from 'app/admin/panel/tickets/admin-panel-all-tickets';
+import AdminPanelSearchTickets from 'app/admin/panel/tickets/admin-panel-search-tickets';
import AdminPanelViewTicket from 'app/admin/panel/tickets/admin-panel-view-ticket';
import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custom-responses';
@@ -113,6 +114,7 @@ export default (
+
diff --git a/client/src/app/admin/panel/admin-panel-menu.js b/client/src/app/admin/panel/admin-panel-menu.js
index da34ccb7..9627947a 100644
--- a/client/src/app/admin/panel/admin-panel-menu.js
+++ b/client/src/app/admin/panel/admin-panel-menu.js
@@ -76,7 +76,7 @@ class AdminPanelMenu extends React.Component {
getGroupItemIndex() {
const group = this.getRoutes()[this.getGroupIndex()];
- const pathname = this.props.location.pathname;
+ const pathname = this.props.location.pathname + this.props.location.search;
return _.findIndex(group.items, {path: pathname});
}
@@ -90,19 +90,35 @@ class AdminPanelMenu extends React.Component {
return (groupIndex === -1) ? 0 : groupIndex;
}
+ getCustomlists() {
+ if(window.customTicketList){
+ return window.customTicketList.map((item, index) => {
+ return {
+ name: item.title,
+ path: '/admin/panel/tickets/search-tickets?custom=' + index,
+ level: 1
+ }
+ })
+ } else {
+ return [];
+ }
+ }
+
getRoutes() {
- return this.getItemsByFilteredByLevel([
+ const customLists = this.getCustomlists();
+
+ return this.getItemsByFilteredByLevel(_.without([
{
groupName: i18n('DASHBOARD'),
path: '/admin/panel',
icon: 'tachometer',
level: 1,
items: this.getItemsByFilteredByLevel([
- {
+ /*{
name: i18n('STATISTICS'),
path: '/admin/panel/stats',
level: 1
- },
+ },*/
{
name: i18n('LAST_ACTIVITY'),
path: '/admin/panel/activity',
@@ -135,10 +151,11 @@ class AdminPanelMenu extends React.Component {
name: i18n('CUSTOM_RESPONSES'),
path: '/admin/panel/tickets/custom-responses',
level: 2
- }
+ },
+ ...customLists
])
},
- {
+ this.props.config['user-system-enabled'] ? {
groupName: i18n('USERS'),
path: '/admin/panel/users',
icon: 'user',
@@ -160,7 +177,7 @@ class AdminPanelMenu extends React.Component {
level: 1
}
])
- },
+ } : null,
{
groupName: i18n('ARTICLES'),
path: '/admin/panel/articles',
@@ -175,7 +192,6 @@ class AdminPanelMenu extends React.Component {
])
},
{
-
groupName: i18n('STAFF'),
path: '/admin/panel/staff',
icon: 'users',
@@ -222,7 +238,7 @@ class AdminPanelMenu extends React.Component {
}
])
}
- ]);
+ ], null));
}
getItemsByFilteredByLevel(items) {
@@ -232,6 +248,7 @@ class AdminPanelMenu extends React.Component {
export default connect((store) => {
return {
- level: store.session.userLevel
+ level: store.session.userLevel,
+ config: store.config
};
})(AdminPanelMenu);
diff --git a/client/src/app/admin/panel/staff/add-staff-modal.js b/client/src/app/admin/panel/staff/add-staff-modal.js
index 8a20ed00..d9c77a0d 100644
--- a/client/src/app/admin/panel/staff/add-staff-modal.js
+++ b/client/src/app/admin/panel/staff/add-staff-modal.js
@@ -67,7 +67,7 @@ class AddStaffModal extends React.Component {
return SessionStore.getDepartments().map(department => {
if(department.private*1){
return
{department.name}
- }else {
+ } else {
return department.name;
}
});
diff --git a/client/src/app/admin/panel/staff/admin-panel-staff-members.js b/client/src/app/admin/panel/staff/admin-panel-staff-members.js
index ac587050..75689f87 100644
--- a/client/src/app/admin/panel/staff/admin-panel-staff-members.js
+++ b/client/src/app/admin/panel/staff/admin-panel-staff-members.js
@@ -11,7 +11,7 @@ import SessionStore from 'lib-app/session-store';
import PeopleList from 'app-components/people-list';
import ModalContainer from 'app-components/modal-container';
-import AddStaffModal from 'app/admin/panel/staff/add-staff-modal';
+import InviteStaffModal from 'app/admin/panel/staff/invite-staff-modal';
import Header from 'core-components/header';
import DropDown from 'core-components/drop-down';
@@ -47,8 +47,8 @@ class AdminPanelStaffMembers extends React.Component {
-
{(this.props.loading) ?
:
this.setState({page: index+1})} />}
@@ -56,8 +56,8 @@ class AdminPanelStaffMembers extends React.Component {
);
}
- onAddNewStaff() {
- ModalContainer.openModal();
+ onInviteStaff() {
+ ModalContainer.openModal();
}
getDepartmentDropdownProps() {
diff --git a/client/src/app/admin/panel/staff/invite-staff-modal.js b/client/src/app/admin/panel/staff/invite-staff-modal.js
new file mode 100644
index 00000000..c98e608c
--- /dev/null
+++ b/client/src/app/admin/panel/staff/invite-staff-modal.js
@@ -0,0 +1,120 @@
+import React from 'react';
+import _ from 'lodash';
+
+import i18n from 'lib-app/i18n';
+import API from 'lib-app/api-call';
+import SessionStore from 'lib-app/session-store';
+
+import Header from 'core-components/header'
+import Form from 'core-components/form';
+import FormField from 'core-components/form-field';
+import SubmitButton from 'core-components/submit-button';
+import Button from 'core-components/button';
+import Icon from 'core-components/icon';
+
+class InviteStaffModal extends React.Component {
+
+ static contextTypes = {
+ closeModal: React.PropTypes.func
+ };
+
+ static propTypes = {
+ onSuccess: React.PropTypes.func
+ };
+
+ state = {
+ loading: false,
+ errors: {},
+ error: null
+ };
+
+ render() {
+ return (
+
+ );
+ }
+
+ getDepartments() {
+ return SessionStore.getDepartments().map(department => {
+ if(department.private*1){
+ return {department.name}
+ } else {
+ return department.name;
+ }
+ });
+ }
+
+ onSubmit(form) {
+ let departments = _.filter(SessionStore.getDepartments(), (department, index) => {
+ return _.includes(form.departments, index);
+ }).map(department => department.id);
+
+ this.setState({loading: true});
+
+ API.call({
+ path: '/staff/invite',
+ data: {
+ name: form.name,
+ email: form.email,
+ level: form.level + 1,
+ departments: JSON.stringify(departments)
+ }
+ }).then(() => {
+ this.context.closeModal();
+
+ if(this.props.onSuccess) {
+ this.props.onSuccess();
+ }
+ }).catch((result) => {
+ this.setState({
+ loading: false,
+ error: result.message
+ });
+ });
+ }
+
+ onCancelClick(event) {
+ event.preventDefault();
+ this.context.closeModal();
+ }
+
+ getErrors() {
+ let errors = _.extend({}, this.state.errors);
+
+ if (this.state.error === 'ALREADY_A_STAFF') {
+ errors.email = i18n('EMAIL_EXISTS');
+ }
+
+ return errors;
+ }
+}
+
+export default InviteStaffModal;
diff --git a/client/src/app/admin/panel/staff/invite-staff-modal.scss b/client/src/app/admin/panel/staff/invite-staff-modal.scss
new file mode 100644
index 00000000..60c35e1c
--- /dev/null
+++ b/client/src/app/admin/panel/staff/invite-staff-modal.scss
@@ -0,0 +1,23 @@
+@import "../../../../scss/vars";
+
+.invite-staff-modal {
+ width: 700px;
+
+ &__level-selector {
+ text-align: center;
+ }
+
+ &__departments {
+ @include scrollbars();
+
+ border: 1px solid $grey;
+ padding: 20px;
+ height: 320px;
+ overflow-y: auto;
+ }
+
+ &__departments-title {
+ font-size: $font-size--md;
+ text-align: center;
+ }
+}
\ No newline at end of file
diff --git a/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js b/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js
new file mode 100644
index 00000000..f5726f2c
--- /dev/null
+++ b/client/src/app/admin/panel/tickets/admin-panel-search-tickets.js
@@ -0,0 +1,34 @@
+import React from 'react';
+import {connect} from 'react-redux';
+
+import i18n from 'lib-app/i18n';
+
+import TicketQueryList from 'app-components/ticket-query-list';
+
+import Header from 'core-components/header';
+import Message from 'core-components/message';
+
+class AdminPanelSearchTickets extends React.Component {
+
+ render() {
+ return (
+
+
+ {(this.props.error) ? {i18n('ERROR_RETRIEVING_TICKETS')} : }
+
+ );
+ }
+
+ getFilters() {
+ let customList = (window.customTicketList && window.customTicketList[this.props.location.query.custom*1]) ? window.customTicketList[this.props.location.query.custom*1] : null
+ return {
+ ...customList
+ };
+ }
+}
+
+export default connect((store) => {
+ return {
+ error: store.adminData.allTicketsError
+ };
+})(AdminPanelSearchTickets);
diff --git a/client/src/app/admin/panel/users/admin-panel-list-users.js b/client/src/app/admin/panel/users/admin-panel-list-users.js
index b2997af2..17872138 100644
--- a/client/src/app/admin/panel/users/admin-panel-list-users.js
+++ b/client/src/app/admin/panel/users/admin-panel-list-users.js
@@ -1,4 +1,5 @@
import React from 'react';
+import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
@@ -12,10 +13,9 @@ import Button from 'core-components/button';
import Message from 'core-components/message';
import Icon from 'core-components/icon';
import ModalContainer from 'app-components/modal-container';
-import MainSignUpWidget from 'app/main/main-signup/main-signup-widget';
+import InviteUserWidget from 'app/admin/panel/users/invite-user-widget';
class AdminPanelListUsers extends React.Component {
-
state = {
loading: true,
users: [],
@@ -39,11 +39,19 @@ class AdminPanelListUsers extends React.Component {
return (
+ {(this.state.error) ? {i18n('ERROR_RETRIEVING_USERS')} : this.renderTableAndInviteButton()}
+
+ );
+ }
+
+ renderTableAndInviteButton() {
+ return (
+
- {(this.state.error) ?
{i18n('ERROR_RETRIEVING_USERS')} :
}
+
-
- {i18n('ADD_USER')}
+
+ {i18n('INVITE_USER')}
@@ -167,17 +175,17 @@ class AdminPanelListUsers extends React.Component {
}).catch(this.onUsersRejected.bind(this)).then(this.onUsersRetrieved.bind(this));
}
- onCreateUser(user) {
+ onInviteUser(user) {
ModalContainer.openModal(
-
-
+
);
}
- onCreateUserSuccess() {
+ onInviteUserSuccess() {
ModalContainer.closeModal();
}
@@ -201,4 +209,8 @@ class AdminPanelListUsers extends React.Component {
}
}
-export default AdminPanelListUsers;
+export default connect((store) => {
+ return {
+ config: store.config
+ };
+})(AdminPanelListUsers);
diff --git a/client/src/app/admin/panel/users/invite-user-widget.js b/client/src/app/admin/panel/users/invite-user-widget.js
new file mode 100644
index 00000000..8336724d
--- /dev/null
+++ b/client/src/app/admin/panel/users/invite-user-widget.js
@@ -0,0 +1,165 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import _ from 'lodash';
+import classNames from 'classnames';
+
+import i18n from 'lib-app/i18n';
+import API from 'lib-app/api-call';
+
+import Captcha from 'app/main/captcha';
+import SubmitButton from 'core-components/submit-button';
+import Message from 'core-components/message';
+import Form from 'core-components/form';
+import FormField from 'core-components/form-field';
+import Widget from 'core-components/widget';
+import Header from 'core-components/header';
+
+class InviteUserWidget extends React.Component {
+
+ static propTypes = {
+ onSuccess: React.PropTypes.func,
+ className: React.PropTypes.string
+ };
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ loading: false,
+ email: null,
+ customFields: []
+ };
+ }
+
+ componentDidMount() {
+ API.call({
+ path: '/system/get-custom-fields',
+ data: {}
+ })
+ .then(result => this.setState({customFields: result.data}));
+ }
+
+ render() {
+ return (
+
+
+
+
+ {this.renderMessage()}
+
+ );
+ }
+
+ renderCustomField(customField, key) {
+ if(customField.type === 'text') {
+ return (
+
+ );
+ } else {
+ const items = customField.options.map(option => ({content: option.name, value: option.name}));
+
+ return (
+
+ );
+ }
+ }
+
+ renderMessage() {
+ switch (this.state.message) {
+ case 'success':
+ return
{i18n('INVITE_USER_SUCCESS')};
+ case 'fail':
+ return
{i18n('EMAIL_EXISTS')};
+ default:
+ return null;
+ }
+ }
+
+ getClass() {
+ let classes = {
+ 'invite-user-widget': true,
+ [this.props.className]: this.props.className
+ };
+ return classNames(classes);
+ }
+
+ getFormProps() {
+ return {
+ loading: this.state.loading,
+ className: 'invite-user-widget__form',
+ onSubmit: this.onInviteUserFormSubmit.bind(this)
+ };
+ }
+
+ getInputProps(password) {
+ return {
+ className: 'invite-user-widget__input',
+ fieldProps: {
+ size: 'medium',
+ password: password
+ }
+ };
+ }
+
+ onInviteUserFormSubmit(formState) {
+ const captcha = this.refs.captcha.getWrappedInstance();
+
+ if (!captcha.getValue()) {
+ captcha.focus();
+ } else {
+ this.setState({
+ loading: true
+ });
+
+ const form = _.clone(formState);
+
+ this.state.customFields.forEach(customField => {
+ if(customField.type === 'select') {
+ form[`customfield_${customField.name}`] = customField.options[form[`customfield_${customField.name}`]].name;
+ }
+ })
+
+ API.call({
+ path: '/user/invite',
+ data: _.extend({captcha: captcha.getValue()}, form)
+ }).then(this.onInviteUserSuccess.bind(this)).catch(this.onInviteUserFail.bind(this));
+ }
+ }
+
+ onInviteUserSuccess() {
+ this.setState({
+ loading: false,
+ message: 'success'
+ });
+ }
+
+ onInviteUserFail() {
+ this.setState({
+ loading: false,
+ message: 'fail'
+ });
+ }
+}
+
+export default InviteUserWidget;
diff --git a/client/src/app/admin/panel/users/invite-user-widget.scss b/client/src/app/admin/panel/users/invite-user-widget.scss
new file mode 100644
index 00000000..9ecbf748
--- /dev/null
+++ b/client/src/app/admin/panel/users/invite-user-widget.scss
@@ -0,0 +1,19 @@
+.invite-user-widget {
+ padding: 30px;
+ text-align: center;
+
+ &__form {
+ margin-bottom: 20px;
+ }
+
+ &__inputs {
+ display: inline-block;
+ margin: 0 auto;
+ }
+
+ &__captcha {
+ margin: 10px auto 20px;
+ height: 78px;
+ width: 304px;
+ }
+}
diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
index 6d30bae1..6fa35f4f 100644
--- a/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
+++ b/client/src/app/main/dashboard/dashboard-create-ticket/create-ticket-form.js
@@ -146,7 +146,7 @@ class CreateTicketForm extends React.Component {
message: 'success'
}, () => {
if(this.props.onSuccess) {
- this.props.onSuccess();
+ this.props.onSuccess(result, email);
}
});
}
diff --git a/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js b/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js
index 57e50d6c..a875f1fb 100644
--- a/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js
+++ b/client/src/app/main/dashboard/dashboard-create-ticket/dashboard-create-ticket-page.js
@@ -32,7 +32,7 @@ class DashboardCreateTicketPage extends React.Component {
);
}
- onCreateTicketSuccess() {
+ onCreateTicketSuccess(result, email) {
if((this.props.location.pathname !== '/create-ticket')) {
setTimeout(() => {history.push('/dashboard')}, 2000);
} else {
diff --git a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js
index 6bf28bfb..75fbd39b 100644
--- a/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js
+++ b/client/src/app/main/dashboard/dashboard-edit-profile/dashboard-edit-profile-page.js
@@ -4,6 +4,7 @@ import _ from 'lodash';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
+import { getCustomFieldParamName } from 'lib-core/APIUtils';
import SessionActions from 'actions/session-actions';
import AreYouSure from 'app-components/are-you-sure';
@@ -42,15 +43,6 @@ class DashboardEditProfilePage extends React.Component {
return (
-
{i18n('ADDITIONAL_FIELDS')}
-
{i18n('EDIT_EMAIL')}
+ {this.state.customFields.length ? this.renderCustomFields() : null}
+
+ );
+ }
+
+ renderCustomFields() {
+ return (
+
+
{i18n('ADDITIONAL_FIELDS')}
+
);
}
@@ -116,9 +125,9 @@ class DashboardEditProfilePage extends React.Component {
customFields.forEach(customField => {
if(customField.type === 'select') {
- parsedFrom[`customfield_${customField.name}`] = customField.options[form[customField.name]].name;
+ parsedFrom[getCustomFieldParamName(customField.name)] = customField.options[form[customField.name]].name;
} else {
- parsedFrom[`customfield_${customField.name}`] = form[customField.name];
+ parsedFrom[getCustomFieldParamName(customField.name)] = form[customField.name];
}
});
diff --git a/client/src/app/main/main-recover-password/main-recover-password-page.js b/client/src/app/main/main-recover-password/main-recover-password-page.js
index e8141c4e..a6807d52 100644
--- a/client/src/app/main/main-recover-password/main-recover-password-page.js
+++ b/client/src/app/main/main-recover-password/main-recover-password-page.js
@@ -30,7 +30,7 @@ class MainRecoverPasswordPage extends React.Component {
render() {
return (