Ivan - Fix AreYouSure design, fix stats, rename get-api-keys, use allow-attachment, fix login verificationToken, fix deletion, fix ticket view permission, fix last events when empty, fix configuration in frontend, fix system preferences and my account

This commit is contained in:
ivan 2017-02-24 03:56:25 -03:00
parent 3d27415041
commit 5126b40538
36 changed files with 169 additions and 91 deletions

View File

@ -1,9 +1,9 @@
export default {
openModal(content) {
openModal(config) {
return {
type: 'OPEN_MODAL',
payload: content
payload: config
}
},

View File

@ -68,7 +68,7 @@ class ActivityRow extends React.Component {
</Link>
</span>
<span className="activity-row__message"> {i18n('ACTIVITY_' + this.props.type)} </span>
{_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : null}
{_.includes(ticketRelatedTypes, this.props.type) ? this.renderTicketNumber() : this.props.to}
<span className="separator" />
</div>
);

View File

@ -19,8 +19,8 @@
&__ticket-link {
}
}
.separator {
margin: 15px;
}
.separator {
margin: 15px;
}
}

View File

@ -1,9 +1,12 @@
import React from 'react';
import i18n from 'lib-app/i18n';
import ModalContainer from 'app-components/modal-container';
import Button from 'core-components/button';
import Input from 'core-components/input';
import ModalContainer from 'app-components/modal-container';
import Icon from 'core-components/icon';
class AreYouSure extends React.Component {
static propTypes = {
@ -26,7 +29,8 @@ class AreYouSure extends React.Component {
static openModal(description, onYes, type) {
ModalContainer.openModal(
<AreYouSure description={description} onYes={onYes} type={type}/>
<AreYouSure description={description} onYes={onYes} type={type}/>,
true
);
}
@ -40,21 +44,25 @@ class AreYouSure extends React.Component {
<div className="are-you-sure__header" id="are-you-sure__header">
{i18n('ARE_YOU_SURE')}
</div>
<span className="are-you-sure__close-icon" onClick={this.onNo.bind(this)}>
<Icon name="times" size="2x"/>
</span>
<div className="are-you-sure__description" id="are-you-sure__description">
{this.props.description || (this.props.type === 'secure' && i18n('PLEASE_CONFIRM_PASSWORD'))}
</div>
{(this.props.type === 'secure') ? this.renderPassword() : null}
<span className="separator" />
<div className="are-you-sure__buttons">
<div className="are-you-sure__yes-button">
<Button type="secondary" size="small" onClick={this.onYes.bind(this)} ref="yesButton" tabIndex="2">
{i18n('YES')}
</Button>
</div>
<div className="are-you-sure__no-button">
<Button type="link" size="auto" onClick={this.onNo.bind(this)} tabIndex="2">
{i18n('CANCEL')}
</Button>
</div>
<div className="are-you-sure__yes-button">
<Button type="secondary" size="small" onClick={this.onYes.bind(this)} ref="yesButton" tabIndex="2">
{i18n('YES')}
</Button>
</div>
</div>
</div>
);
@ -62,7 +70,7 @@ class AreYouSure extends React.Component {
renderPassword() {
return (
<Input className="are-you-sure__password" password placeholder="password" name="password" value={this.state.password} onChange={this.onPasswordChange.bind(this)} onKeyDown={this.onInputKeyDown.bind(this)}/>
<Input className="are-you-sure__password" password placeholder="password" name="password" ref="password" size="medium" value={this.state.password} onChange={this.onPasswordChange.bind(this)} onKeyDown={this.onInputKeyDown.bind(this)}/>
);
}
@ -79,7 +87,11 @@ class AreYouSure extends React.Component {
}
onYes() {
if (this.props.type === 'default' || this.state.password){
if (this.props.type === 'secure' && !this.state.password) {
this.refs.password.focus()
}
if (this.props.type === 'default' || this.state.password) {
this.closeModal();
if (this.props.onYes) {

View File

@ -1,25 +1,30 @@
@import "../scss/vars";
.are-you-sure {
width: 400px;
text-align: center;
width: 800px;
text-align: left;
&__header {
color: $primary-red;
font-size: $font-size--xl;
background-color: $secondary-blue;
border-top-right-radius: 4px;
border-top-left-radius: 4px;
color: white;
font-size: $font-size--lg;
font-weight: bold;
margin-bottom: 20px;
margin-bottom: 10px;
padding: 10px;
}
&__description {
color: $dark-grey;
font-size: $font-size--md;
margin-bottom: 50px;
padding: 14px 5% 0;
}
&__buttons {
margin: 0 auto;
margin-top: 10px;
padding-bottom: 10px;
text-align: right;
}
&__yes-button,
@ -30,7 +35,25 @@
&__password {
margin: 0 auto;
margin-top: -30px;
margin-bottom: 20px;
margin-top: 20px;
}
&__close-icon {
cursor: pointer;
position: absolute;
top: 10px;
right: 10px;
color: white;
}
.separator {
width: 90%;
margin: 30px auto;
}
}
@media screen and (max-width: 800px) {
.are-you-sure {
width: auto;
}
}

View File

@ -1,6 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import keyCode from 'keycode';
import classNames from 'classnames';
import store from 'app/store';
import ModalActions from 'actions/modal-actions';
@ -8,11 +9,12 @@ import Modal from 'core-components/modal';
class ModalContainer extends React.Component {
static openModal(content) {
static openModal(content, noPadding) {
store.dispatch(
ModalActions.openModal(
content
)
ModalActions.openModal({
content,
noPadding
})
);
}
@ -48,7 +50,7 @@ class ModalContainer extends React.Component {
renderModal() {
return (
<Modal content={this.props.modal.content} />
<Modal content={this.props.modal.content} noPadding={this.props.modal.noPadding}/>
);
}

View File

@ -13,7 +13,7 @@ class PeopleList extends React.Component {
name: React.PropTypes.node,
assignedTickets: React.PropTypes.number,
closedTickets: React.PropTypes.number,
lastLogin: React.PropTypes.number
lastLogin: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.string])
})),
pageSize: React.PropTypes.number,
page: React.PropTypes.number,

View File

@ -1,6 +1,7 @@
import React from 'react';
import _ from 'lodash';
import RichTextEditor from 'react-rte-browserify';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
@ -190,7 +191,7 @@ class TicketViewer extends React.Component {
<div className="ticket-viewer__response-field row">
<Form {...this.getCommentFormProps()}>
<FormField name="content" validation="TEXT_AREA" required field="textarea" />
<FormField name="file" field="file"/>
{(this.props.allowAttachments) ? <FormField name="file" field="file"/> : null}
<SubmitButton>{i18n('RESPOND_TICKET')}</SubmitButton>
</Form>
</div>
@ -333,7 +334,9 @@ class TicketViewer extends React.Component {
onCommentSuccess() {
this.setState({
loading: false,
commentError: false
commentValue: RichTextEditor.createEmptyValue(),
commentError: false,
commentEdited: false
});
this.onTicketModification();
@ -353,4 +356,8 @@ class TicketViewer extends React.Component {
}
}
export default TicketViewer;
export default connect((store) => {
return {
allowAttachments: store.config['allow-attachments']
};
})(TicketViewer);

View File

@ -23,11 +23,11 @@ class AdminPanelMyAccount extends React.Component {
getEditorProps() {
return {
myAccount: true,
staffId: this.props.userId,
staffId: this.props.userId * 1,
name: this.props.userName,
email: this.props.userEmail,
profilePic: this.props.userProfilePic,
level: this.props.userLevel,
level: this.props.userLevel * 1,
departments: this.props.userDepartments,
onChange: () => this.props.dispatch(SessionActions.getUserData(null, null, true))
};

View File

@ -8,7 +8,7 @@ class AdminPanelStats extends React.Component {
render() {
return (
<div class="admin-panel-stats">
<div className="admin-panel-stats">
<Header title={i18n('STATISTICS')} description={i18n('STATISTICS_DESCRIPTION')}/>
<Stats type="general"/>
</div>

View File

@ -213,15 +213,15 @@ class AdminPanelSystemPreferences extends React.Component {
'reCaptchaPrivate': result.data.reCaptchaPrivate,
'url': result.data['url'],
'title': result.data['title'],
'layout': result.data['layout'] == 'full-width' ? 1 : 0,
'layout': (result.data['layout'] == 'full-width') ? 1 : 0,
'time-zone': result.data['time-zone'],
'no-reply-email': result.data['no-reply-email'],
'smtp-host': result.data['smtp-host'],
'smtp-port': result.data['smtp-port'],
'smtp-user': result.data['smtp-user'],
'smtp-pass': '',
'maintenance-mode': result.data['maintenance-mode'],
'allow-attachments': result.data['allow-attachments'],
'maintenance-mode': !!(result.data['maintenance-mode'] * 1),
'allow-attachments': !!(result.data['allow-attachments'] * 1),
'max-size': result.data['max-size'],
'allowedLanguages': result.data.allowedLanguages.map(lang => (_.indexOf(languageKeys, lang))),
'supportedLanguages': result.data.supportedLanguages.map(lang => (_.indexOf(languageKeys, lang)))

View File

@ -9,6 +9,7 @@
&__update-name-button {
float: left;
min-width: 156px;
}
&__optional-buttons {

View File

@ -68,9 +68,9 @@ class AdminPanelStaffMembers extends React.Component {
if(!this.state.selectedDepartment) {
staffList = this.state.staffList;
} else {
staffList = _.filter(this.state.staffList, (o) => {
return _.findIndex(o.departments, {id: this.state.selectedDepartment}) !== -1;
})
staffList = _.filter(this.state.staffList, (staff) => {
return _.findIndex(staff.departments, {id: this.state.selectedDepartment}) !== -1;
});
}
return staffList.map(staff => {

View File

@ -31,6 +31,10 @@ class StaffEditor extends React.Component {
onDelete: React.PropTypes.func
};
static defaultProps = {
tickets: []
};
state = {
email: this.props.email,
level: this.props.level - 1,

View File

@ -151,8 +151,8 @@ class AdminPanelListUsers extends React.Component {
onUsersRetrieved(result) {
this.setState({
page: result.data.page,
pages: result.data.pages,
page: result.data.page * 1,
pages: result.data.pages * 1,
users: result.data.users,
orderBy: result.data.orderBy,
desc: (result.data.desc === '1'),

View File

@ -153,9 +153,7 @@ let DemoPage = React.createClass({
title: 'ModalTrigger',
render: (
<Button onClick={function () {
ModalContainer.openModal(
<AreYouSure description="I confirm I want to perform this action." onYes={()=> {alert('yes');}} />
);
AreYouSure.openModal('I confirm I want to perform this action.', ()=> {alert('yes');}, 'secure')
}}>
Open Modal
</Button>

View File

@ -3,7 +3,6 @@ import React from 'react';
import API from 'lib-app/api-call';
import i18n from 'lib-app/i18n';
import ModalContainer from 'app-components/modal-container';
import AreYouSure from 'app-components/are-you-sure';
import Header from 'core-components/header';
@ -64,11 +63,11 @@ class DashboardEditProfilePage extends React.Component {
}
}
onSubmitEditEmail(formState) {
ModalContainer.openModal(<AreYouSure onYes={this.callEditEmailAPI.bind(this, formState)}/>);
AreYouSure.openModal(i18n('EMAIL_WILL_CHANGE'), this.callEditEmailAPI.bind(this, formState));
}
onSubmitEditPassword(formState) {
ModalContainer.openModal(<AreYouSure onYes={this.callEditPassAPI.bind(this, formState)}/>);
AreYouSure.openModal(i18n('PASSWORD_WILL_CHANGE'), this.callEditPassAPI.bind(this, formState));
}
callEditEmailAPI(formState){

View File

@ -32,7 +32,7 @@ class MainLayoutHeader extends React.Component {
result = (
<div className="main-layout-header__login-links">
<Button type="clean" route={{to:'/'}}>{i18n('LOG_IN')}</Button>
{(this.props.config['registration']) ? <Button type="clean" route={{to:'/signup'}}>{i18n('SIGN_UP')}</Button> : null}
{(!!(this.props.config['registration'] * 1)) ? <Button type="clean" route={{to:'/signup'}}>{i18n('SIGN_UP')}</Button> : null}
</div>
);
}

View File

@ -1,10 +1,12 @@
import React from 'react';
import classNames from 'classnames';
import {Motion, spring} from 'react-motion';
class Modal extends React.Component {
static propTypes = {
content: React.PropTypes.node
content: React.PropTypes.node,
noPadding: React.PropTypes.bool
};
render() {
@ -30,13 +32,22 @@ class Modal extends React.Component {
renderModal(animation) {
return (
<div className="modal" style={{opacity: animation.fade}}>
<div className={this.getClass()} style={{opacity: animation.fade}}>
<div className="modal__content" style={{transform: 'scale(' + animation.scale + ')'}}>
{this.props.content}
</div>
</div>
)
}
getClass() {
let classes = {
'modal': true,
'modal_no-padding': this.props.noPadding
};
return classNames(classes);
}
}
export default Modal;

View File

@ -18,4 +18,11 @@
padding: 50px;
box-shadow: 0 0 10px white;
}
&_no-padding {
.modal__content {
padding: 0;
}
}
}

View File

@ -281,7 +281,9 @@ export default {
'TICKET_SENT': 'Ticket has been created successfully.',
'VALID_RECOVER': 'Password recovered successfully',
'EMAIL_EXISTS': 'Email already exists',
'ARE_YOU_SURE': 'Are you sure?',
'ARE_YOU_SURE': 'Confirm action',
'EMAIL_WILL_CHANGE': 'The current email will be changed',
'PASSWORD_WILL_CHANGE': 'The current password will be changed',
'EMAIL_CHANGED': 'Email has been changed successfully',
'PASSWORD_CHANGED': 'Password has been changed successfully',
'OLD_PASSWORD_INCORRECT': 'Old password is incorrect',

View File

@ -29,6 +29,7 @@ function processData (data, dataAsForm = false) {
module.exports = {
call: function ({path, data, plain, dataAsForm}) {
console.log('request ' + path, data);
return new Promise(function (resolve, reject) {
APIUtils.post(apiUrl + path, processData(data, dataAsForm), dataAsForm)
.then(function (result) {

View File

@ -62,6 +62,7 @@ class SessionStore {
this.setItem('title', configs.title);
this.setItem('registration', configs.registration);
this.setItem('user-system-enabled', configs['user-system-enabled']);
this.setItem('allow-attachments', configs['allow-attachments']);
}
getConfigs() {
@ -72,9 +73,11 @@ class SessionStore {
allowedLanguages: JSON.parse(this.getItem('allowedLanguages')),
supportedLanguages: JSON.parse(this.getItem('supportedLanguages')),
layout: this.getItem('layout'),
registration: this.getItem('registration'),
title: this.getItem('title'),
['user-system-enabled']: this.getItem('user-system-enabled')
registration: !!(this.getItem('registration') * 1),
'user-system-enabled': !!(this.getItem('user-system-enabled') * 1),
'allow-attachments': !!(this.getItem('allow-attachments') * 1),
'maintenance-mode': !!(this.getItem('maintenance-mode') * 1)
};
}

View File

@ -26,7 +26,6 @@ class AdminDataReducer extends Reducer {
getTypeHandlers() {
return {
'CUSTOM_RESPONSES_FULFILLED': this.onCustomResponses,
'SESSION_CHECKED': this.onSessionChecked,
'MY_TICKETS_FULFILLED': this.onMyTicketsRetrieved,
'MY_TICKETS_REJECTED': this.onMyTicketsRejected,
@ -51,15 +50,6 @@ class AdminDataReducer extends Reducer {
});
}
onSessionChecked(state) {
const customResponses = sessionStore.getItem('customResponses');
return _.extend({}, state, {
customResponses: JSON.parse(customResponses),
customResponsesLoaded: true
});
}
onMyTicketsRetrieved(state, payload) {
return _.extend({}, state, {
myTickets: payload.data,

View File

@ -37,6 +37,10 @@ class ConfigReducer extends Reducer {
return _.extend({}, state, payload.data, {
language: currentLanguage || payload.language,
registration: !!(payload.data.registration * 1),
'user-system-enabled': !!(payload.data['user-system-enabled']* 1),
'allow-attachments': !!(payload.data['allow-attachments']* 1),
'maintenance-mode': !!(payload.data['maintenance-mode']* 1),
initDone: true
});
}

View File

@ -7,6 +7,7 @@ class ModalReducer extends Reducer {
getInitialState() {
return {
opened: false,
noPadding: false,
content: null
};
}
@ -23,7 +24,8 @@ class ModalReducer extends Reducer {
return _.extend({}, state, {
opened: true,
content: payload
content: payload.content,
noPadding: payload.noPadding || false
});
}
@ -32,7 +34,8 @@ class ModalReducer extends Reducer {
return _.extend({}, state, {
opened: false,
content: null
content: null,
noPadding: false
});
}
}

View File

@ -28,8 +28,11 @@ class LastEventsStaffController extends Controller {
$query = substr($query,0,-3);
$query .= ') ORDER BY id desc LIMIT ? OFFSET ?' ;
$eventList = Ticketevent::find($query, [10, 10*($page-1)]);
Response::respondSuccess($eventList->toArray());
if(Ticketevent::count() && !$user->sharedTicketList->isEmpty()) {
$eventList = Ticketevent::find($query, [10, 10*($page-1)]);
Response::respondSuccess($eventList->toArray());
} else {
Response::respondSuccess([]);
}
}
}

View File

@ -15,7 +15,7 @@ require_once 'system/disable-user-system.php';
require_once 'system/enabled-user-system.php';
require_once 'system/add-api-key.php';
require_once 'system/delete-api-key.php';
require_once 'system/get-all-keys.php';
require_once 'system/get-api-keys.php';
require_once 'system/get-stats.php';
require_once 'system/delete-all-users.php';
require_once 'system/csv-import.php';
@ -40,7 +40,7 @@ $systemControllerGroup->addController(new EnableRegistrationController);
$systemControllerGroup->addController(new GetStatsController);
$systemControllerGroup->addController(new AddAPIKeyController);
$systemControllerGroup->addController(new DeleteAPIKeyController);
$systemControllerGroup->addController(new GetAllKeyController);
$systemControllerGroup->addController(new GetAPIKeysController);
$systemControllerGroup->addController(new DeleteAllUsersController);
$systemControllerGroup->addController(new BackupDatabaseController);
$systemControllerGroup->addController(new DownloadController);

View File

@ -1,8 +1,8 @@
<?php
use Respect\Validation\Validator as DataValidator;
class GetAllKeyController extends Controller {
const PATH = '/get-all-keys';
class GetAPIKeysController extends Controller {
const PATH = '/get-api-keys';
const METHOD = 'POST';
public function validations() {

View File

@ -86,6 +86,6 @@ class TicketGetController extends Controller {
$user = Controller::getLoggedUser();
return (!Controller::isStaffLogged() && (Controller::isUserSystemEnabled() && $this->ticket->author->id !== $user->id)) ||
(Controller::isStaffLogged() && $this->ticket->owner && $this->ticket->owner->id !== $user->id);
(Controller::isStaffLogged() && !$user->sharedDepartmentList->includesId($this->ticket->department->id));
}
}

View File

@ -30,6 +30,11 @@ class DeleteUserController extends Controller {
Log::createLog('DELETE_USER', $user->name);
RedBean::exec('DELETE FROM log WHERE author_user_id = ?', [$userId]);
foreach($user->sharedTicketList as $ticket) {
$ticket->delete();
}
$user->delete();
Response::respondSuccess();

View File

@ -24,6 +24,11 @@ class LoginController extends Controller {
}
if ($this->checkInputCredentials() || $this->checkRememberToken()) {
if($this->userInstance->verificationToken !== null) {
Response::respondError(ERRORS::UNVERIFIED_USER);
return;
}
$this->createUserSession();
$this->createSessionCookie();
if(Controller::request('staff')) {
@ -31,14 +36,6 @@ class LoginController extends Controller {
$this->userInstance->store();
}
$email = Controller::request('email');
$userRow = User::getDataStore($email, 'email');
if($userRow->verificationToken !== null) {
Response::respondError(ERRORS::UNVERIFIED_USER);
return;
}
Response::respondSuccess($this->getUserData());
} else {
Response::respondError(ERRORS::INVALID_CREDENTIALS);

View File

@ -79,7 +79,9 @@ abstract class Controller {
}
public function uploadFile() {
if(!isset($_FILES['file'])) return '';
$allowAttachments = Setting::getSetting('allow-attachments')->getValue();
if(!isset($_FILES['file']) || !$allowAttachments) return '';
$maxSize = Setting::getSetting('max-size')->getValue();
$fileGap = Setting::getSetting('file-gap')->getValue();

View File

@ -45,6 +45,10 @@ class DataStoreList implements IteratorAggregate {
return $includes;
}
public function isEmpty() {
return empty($list);
}
public function toBeanList() {
$beanList = [];

View File

@ -55,7 +55,7 @@ require './system/disable-registration.rb'
require './system/enable-registration.rb'
require './system/add-api-key.rb'
require './system/delete-api-key.rb'
require './system/get-all-keys.rb'
require './system/get-api-keys.rb'
require './system/file-upload-download.rb'
require './system/csv-import.rb'
require './system/disable-user-system.rb'

View File

@ -1,15 +1,15 @@
describe'system/get-all-keys' do
describe'system/get-api-keys' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
it 'should get all API keys' do
it 'should get all API keys' do
Scripts.createAPIKey('namekey1')
Scripts.createAPIKey('namekey2')
Scripts.createAPIKey('namekey3')
Scripts.createAPIKey('namekey4')
Scripts.createAPIKey('namekey5')
result= request('/system/get-all-keys', {
result = request('/system/get-api-keys', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
})