Merge pull request #6 from ivandiazwm/guillermo-master

add-tag path
This commit is contained in:
Guillermo Giuliana 2019-02-24 22:32:37 -03:00 committed by GitHub
commit 1d3d18b345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1039 additions and 78 deletions

View File

@ -1,5 +1,6 @@
import React from 'react';
import _ from 'lodash';
import {connect} from "react-redux";
import i18n from 'lib-app/i18n';
import DateTransformer from 'lib-core/date-transformer';
@ -11,6 +12,7 @@ import Button from 'core-components/button';
import Tooltip from 'core-components/tooltip';
import Icon from 'core-components/icon';
import Checkbox from 'core-components/checkbox';
import Tag from 'core-components/tag';
class TicketList extends React.Component {
static propTypes = {
@ -182,9 +184,16 @@ class TicketList extends React.Component {
</Tooltip>
),
title: (
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
{titleText}
</Button>
<div>
<Button className="ticket-list__title-link" type="clean" route={{to: this.props.ticketPath + ticket.ticketNumber}}>
{titleText}
</Button>
{ticket.tags.map((tagName,index) => {
let tag = _.find(this.props.tags, {name:tagName});
return <Tag size='small' name={tag && tag.name} color={tag && tag.color} key={index} />
})}
</div>
),
priority: this.getTicketPriority(ticket.priority),
department: ticket.department.name,
@ -257,5 +266,8 @@ class TicketList extends React.Component {
}
}
export default TicketList;
export default connect((store) => {
return {
tags: store.config['tags']
};
})(TicketList);

View File

@ -22,6 +22,8 @@ import Icon from 'core-components/icon';
import TextEditor from 'core-components/text-editor';
import InfoTooltip from 'core-components/info-tooltip';
import DepartmentDropdown from 'app-components/department-dropdown';
import TagSelector from 'core-components/tag-selector';
import Tag from 'core-components/tag';
class TicketViewer extends React.Component {
static propTypes = {
@ -36,10 +38,12 @@ class TicketViewer extends React.Component {
userId: React.PropTypes.number,
userStaff: React.PropTypes.bool,
userDepartments: React.PropTypes.array,
userLevel: React.PropTypes.number
userLevel: React.PropTypes.number,
tags: React.PropTypes.array
};
static defaultProps = {
tags: [],
editable: false,
ticket: {
author: {},
@ -63,6 +67,7 @@ class TicketViewer extends React.Component {
render() {
const ticket = this.props.ticket;
console.log('tickett',ticket)
return (
<div className="ticket-viewer">
@ -105,7 +110,7 @@ class TicketViewer extends React.Component {
<div className="ticket-viewer__info-row-header row">
<div className="col-md-4">{i18n('DEPARTMENT')}</div>
<div className="col-md-4">{i18n('AUTHOR')}</div>
<div className="col-md-4">{i18n('DATE')}</div>
<div className="col-md-4">{i18n('TAGS')}</div>
</div>
<div className="ticket-viewer__info-row-values row">
<div className="col-md-4">
@ -115,7 +120,7 @@ class TicketViewer extends React.Component {
onChange={this.onDepartmentDropdownChanged.bind(this)} />
</div>
<div className="col-md-4">{ticket.author.name}</div>
<div className="col-md-4">{DateTransformer.transformToString(ticket.date)}</div>
<div className="col-md-4"> <TagSelector items={this.props.tags} values={this.props.ticket.tags} onRemoveClick={this.removeTag.bind(this)} onTagSelected={this.addTag.bind(this)}/></div>
</div>
<div className="ticket-viewer__info-row-header row">
<div className="col-md-4">{i18n('PRIORITY')}</div>
@ -153,12 +158,15 @@ class TicketViewer extends React.Component {
<div className="ticket-viewer__info-row-header row">
<div className="ticket-viewer__department col-md-4">{i18n('DEPARTMENT')}</div>
<div className="ticket-viewer__author col-md-4">{i18n('AUTHOR')}</div>
<div className="ticket-viewer__date col-md-4">{i18n('DATE')}</div>
<div className="ticket-viewer__date col-md-4">{i18n('TAGS')}</div>
</div>
<div className="ticket-viewer__info-row-values row">
<div className="ticket-viewer__department col-md-4">{ticket.department.name}</div>
<div className="ticket-viewer__author col-md-4">{ticket.author.name}</div>
<div className="ticket-viewer__date col-md-4">{DateTransformer.transformToString(ticket.date, false)}</div>
<div className="col-md-4">{ticket.tags.length ? ticket.tags.map((tagName,index) => {
let tag = _.find(this.props.tags, {name:tagName});
return <Tag name={tag && tag.name} color={tag && tag.color} key={index} />
}) : i18n('NONE')}</div>
</div>
<div className="ticket-viewer__info-row-header row">
<div className="ticket-viewer__department col-md-4">{i18n('PRIORITY')}</div>
@ -412,7 +420,25 @@ class TicketViewer extends React.Component {
}
}).then(this.onTicketModification.bind(this));
}
addTag(tag) {
API.call({
path: '/ticket/add-tag',
data: {
ticketNumber: this.props.ticket.ticketNumber,
tagId: tag
}
}).then(this.onTicketModification.bind(this))
}
removeTag(tag) {
API.call({
path: '/ticket/remove-tag',
data: {
ticketNumber: this.props.ticket.ticketNumber,
tagId: tag
}
}).then(this.onTicketModification.bind(this))
}
onCustomResponsesChanged({index}) {
let replaceContentWithCustomResponse = () => {
this.setState({
@ -515,6 +541,7 @@ export default connect((store) => {
staffMembersLoaded: store.adminData.staffMembersLoaded,
allowAttachments: store.config['allow-attachments'],
userSystemEnabled: store.config['user-system-enabled'],
userLevel: store.session.userLevel
userLevel: store.session.userLevel,
tags: store.config['tags']
};
})(TicketViewer);

View File

@ -35,6 +35,7 @@ import AdminPanelNewTickets from 'app/admin/panel/tickets/admin-panel-new-ticket
import AdminPanelAllTickets from 'app/admin/panel/tickets/admin-panel-all-tickets';
import AdminPanelViewTicket from 'app/admin/panel/tickets/admin-panel-view-ticket';
import AdminPanelCustomResponses from 'app/admin/panel/tickets/admin-panel-custom-responses';
import AdminPanelCustomTags from 'app/admin/panel/tickets/admin-panel-custom-tags';
import AdminPanelListUsers from 'app/admin/panel/users/admin-panel-list-users';
import AdminPanelViewUser from 'app/admin/panel/users/admin-panel-view-user';
@ -114,6 +115,7 @@ export default (
<Route path="all-tickets" component={AdminPanelAllTickets} />
<Route path="custom-responses" component={AdminPanelCustomResponses} />
<Route path="view-ticket/:ticketNumber" component={AdminPanelViewTicket} />
<Route path="custom-tags" component={AdminPanelCustomTags} />
</Route>
<Route path="users">

View File

@ -135,6 +135,11 @@ class AdminPanelMenu extends React.Component {
name: i18n('CUSTOM_RESPONSES'),
path: '/admin/panel/tickets/custom-responses',
level: 2
},
{
name: 'Customtags i18n',
path: '/admin/panel/tickets/custom-tags',
level: 1
}
])
},

View File

@ -0,0 +1,261 @@
import React from 'react';
import _ from 'lodash';
import {connect} from 'react-redux';
import i18n from 'lib-app/i18n';
import API from 'lib-app/api-call';
import AdminDataActions from 'actions/admin-data-actions';
import AreYouSure from 'app-components/are-you-sure';
import LanguageSelector from 'app-components/language-selector';
import Icon from 'core-components/icon';
import Button from 'core-components/button';
import Header from 'core-components/header';
import Listing from 'core-components/listing';
import Loading from 'core-components/loading';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
import SubmitButton from 'core-components/submit-button';
import TextEditor from 'core-components/text-editor';
import ModalContainer from 'app-components/modal-container';
import ColorSelector from 'core-components/color-selector';
class TagList extends React.Component {
constructor(props) {
super(props);
const list = this.props.tags.map((tag) => {
<h1>{tag.name}</h1>;
})
}
render() {
return (
<div>
{list}
</div>
);
}
}
class AdminPanelCustomTags extends React.Component {
static defaultProps = {
items: [],
};
state = {
formClicked: false,
showForm: false,
formLoading: false,
selectedIndex: -1,
errors: {},
originalForm: {
title: '',
content: TextEditor.createEmpty(),
language: this.props.language
},
form: {
title: '',
content: TextEditor.createEmpty(),
language: this.props.language
},
tagList: {} //
};
componentDidMount() {
if (!this.props.loaded) {
this.retrieveCustomResponses();
}
}
render() {
return (
<div className="admin-panel-custom-tags">
<Header title={i18n('CUSTOM_TAGS')} description={i18n('CUSTOM_TAGS_DESCRIPTION')} />
{(this.props.loaded) ? this.renderContent() : this.renderLoading()}
</div>
);
}
renderContent() {
return (
<div className="row">
<div className="col-md-3">
<Button onClick={this.onCreateTag.bind(this)} type="secondary" ><Icon name="pencil"/>'NUEVO TAGi18n'</Button>
<TagList tags={this.state.tagList}/>
</div>
</div>
);
}
onCreateTag() {
ModalContainer.openModal(
<div>
<Header title={'new tags'} description={i18n('Here you can add a topic that works as a category for articles.')} />
<Form values={this.state.values} onChange={this.onFormChange.bind(this)} onSubmit={this.onSubmitTag.bind(this)} loading={this.state.loading}>
<FormField name="title" label={i18n('TITLE')} fieldProps={{size: 'large'}} validation="TITLE" required />
<FormField name="color" className="topic-edit-modal__color" label={i18n('COLOR')} decorator={ColorSelector} />
<FormField className="topic-edit-modal__private" label={i18n('PRIVATE')} name="private" field="checkbox"/>
<SubmitButton className="topic-edit-modal__save-button" type="secondary" size="small">
{i18n('SAVE')}
</SubmitButton>
<Button className="topic-edit-modal__discard-button" onClick={this.onDiscardClick.bind(this)} size="small">
{i18n('CANCEL')}
</Button>
</Form>
</div>
);
}
onSubmitTag() {
API.call({
path: '/ticket/add-tag',
data: {
name: form.title,
color: form.content,
}
}).then(() => {
this.context.closeModal();
this.updateTagList();
if(this.props.onChange) {
this.props.onChange();
}
}).catch(() => {
this.setState({
loading: false
});
});
}
onFormChange(form) {
this.setState({
values: form
});
}
onDiscardClick(event) {
event.preventDefault();
this.context.closeModal();
}
renderLoading() {
return (
<div className="admin-panel-custom-responses__loading">
<Loading backgrounded size="large"/>
</div>
);
}
renderOptionalButtons() {
return (
<div className="admin-panel-custom-responses__optional-buttons">
<div className="admin-panel-custom-responses__discard-button">
{this.isEdited() ? <Button onClick={this.onDiscardChangesClick.bind(this)}>{i18n('DISCARD_CHANGES')}</Button> : null}
</div>
<div className="admin-panel-custom-responses__delete-button">
<Button onClick={this.onDeleteClick.bind(this)}>{i18n('DELETE')}</Button>
</div>
</div>
);
}
onItemChange(index) {
if(this.isEdited()) {
AreYouSure.openModal(i18n('WILL_LOSE_CHANGES'), this.updateForm.bind(this, index));
} else {
this.updateForm(index);
}
}
onSubmit() {
this.setState({
loading: true
});
API.call({
path: (this.props.addForm) ? '/article/add-topic' : '/article/edit-topic',
data: {
topicId: this.props.topicId,
name: this.state.values['title'],
icon: this.state.values['icon'],
iconColor: this.state.values['color'],
private: this.state.values['private']*1
}
}).then(() => {
this.context.closeModal();
if(this.props.onChange) {
this.props.onChange();
}
}).catch(() => {
this.setState({
loading: false
});
});
}
onDiscardChangesClick(event) {
event.preventDefault();
this.onItemChange(this.state.selectedIndex);
}
onDeleteClick(event) {
event.preventDefault();
AreYouSure.openModal(i18n('WILL_DELETE_CUSTOM_RESPONSE'), this.deleteCustomResponse.bind(this));
}
deleteCustomResponse() {
API.call({
path: '/ticket/delete-custom-response',
data: {
id: this.props.items[this.state.selectedIndex].id
}
}).then(() => {
this.retrieveCustomResponses();
this.onItemChange(-1);
});
}
updateTagList() {
API.call({
path: '/ticket/get-tags'
}).then(() => {
this.setState({
tagList: data
});
});
}
updateForm(index) {
let form = _.clone(this.state.form);
form.title = (this.props.items[index] && this.props.items[index].name) || '';
form.content = TextEditor.getEditorStateFromHTML((this.props.items[index] && this.props.items[index].content) || '');
form.language = (this.props.items[index] && this.props.items[index].language) || this.props.language;
this.setState({
formClicked: false,
showForm: true,
selectedIndex: index,
formLoading: false,
originalForm: form,
form: form,
errors: {}
});
}
retrieveCustomResponses() {
this.props.dispatch(AdminDataActions.retrieveCustomResponses());
}
isEdited() {
return this.state.form.title && this.state.formClicked && (
this.state.form.title != this.state.originalForm.title ||
this.state.form.content != this.state.originalForm.content ||
this.state.form.language != this.state.originalForm.language
);
}
}
export default connect((store) => {
return {
allowedLanguages: store.config.allowedLanguages,
language: store.config.language,
loaded: store.adminData.customResponsesLoaded,
items: store.adminData.customResponses
};
})(AdminPanelCustomTags);

View File

@ -0,0 +1,31 @@
.admin-panel-custom-tags {
&__loading {
height: 300px;
}
&__item-flag {
float: right;
}
&__actions {
text-align: left;
}
&__save-button {
display: inline-block;
margin-right: 30px;
}
&__optional-buttons {
display: inline;
}
&__discard-button {
display: inline-block;
}
&__delete-button {
display: inline-block;
float: right;
}
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import _ from 'lodash';
import Icon from 'core-components/icon';
import DropDown from 'core-components/drop-down';
import Tag from 'core-components/tag';
class TagSelector extends React.Component {
@ -32,14 +33,8 @@ class TagSelector extends React.Component {
renderSelectedTag(item,index) {
return (
<div className="tag-selector__selected-tag" style={{backgroundColor:item.color}} onClick={event => event.stopPropagation()} key={index}>
<span className="tag-selector__selected-tag-name">{item.name}</span>
<span onClick={this.onRemoveClick.bind(this,item.name)} className="tag-selector__selected-tag-remove" >
<Icon name="times-circle" size="small"/>
</span>
</div>
);
return <Tag name={item.name} color={item.color} showDeleteButton onRemoveClick={this.onRemoveClick.bind(this,item.id)} key={index}/>;
}
renderTagOptions() {
@ -50,24 +45,23 @@ class TagSelector extends React.Component {
renderTagOption(item,index) {
return (
<div onClick={this.onTagSelected.bind(this,item.name)} className="tag-selector__tag-option" key={index}>
<div onClick={this.onTagSelected.bind(this,item.id)} className="tag-selector__tag-option" key={index}>
<span className="tag-selector__tag-option-square" style={{backgroundColor:item.color}}/>
<span className="tag-selector__tag-option-name" >{item.name}</span>
</div>
);
}
onRemoveClick(tag) {
onRemoveClick(tagId) {
if(this.props.onRemoveClick){
this.props.onRemoveClick(tag);
}
}
onTagSelected(tag) {
if(this.props.onTagSelected){
this.props.onTagSelected(tag);
this.props.onRemoveClick(tagId);
}
}
onTagSelected(tagId) {
if(this.props.onTagSelected){
this.props.onTagSelected(tagId);
}
}
}
export default TagSelector;

View File

@ -9,6 +9,7 @@
cursor: text;
background-color: white;
border: 1px solid $grey;
min-height: 38px;
&:focus {
outline: none;
@ -17,34 +18,6 @@
}
}
&__selected-tags {
border-radius: 3px;
background-color: white;
padding: 3px 1px;
}
&__selected-tag {
color: white;
display: inline-block;
border-radius: 3px;
margin-left: 5px;
padding: 3px;
font-size: 13px;
}
&__selected-tag {
cursor: default;
&-remove {
cursor: pointer;
margin-left: 10px;
&:hover {
color: $light-grey;
}
}
}
&__tag-options {
font-size: 13px;
color: $dark-grey;

View File

@ -0,0 +1,42 @@
import React from 'react';
import Icon from 'core-components/icon';
import classNames from 'classnames';
class Tag extends React.Component {
static propTypes = {
name: React.PropTypes.string,
color: React.PropTypes.string,
showDeleteButton: React.PropTypes.bool,
onRemoveClick: React.PropTypes.func,
size: React.PropTypes.oneOf(['small','medium'])
};
render() {
return (
<div className={this.getClass()} style={{backgroundColor:this.props.color}} onClick={event => event.stopPropagation()} >
<span className="tag__name">{this.props.name}</span>
{this.props.showDeleteButton ? this.renderRemoveButton() : null}
</div>
);
}
renderRemoveButton() {
return (
<span onClick={this.props.onRemoveClick} className="tag__remove" >
<Icon name="times-circle" size="small"/>
</span>
);
}
getClass() {
let classes = {
'tag': true,
'tag_small': this.props.size === 'small',
'tag_medium': this.props.size === 'medium'
};
return classNames(classes);
}
}
export default Tag;

View File

@ -0,0 +1,24 @@
@import '../scss/vars';
.tag{
color: white;
display: inline-block;
border-radius: 3px;
margin-left: 5px;
padding: 3px;
font-size: 13px;
cursor: default;
&__remove {
cursor: pointer;
margin-left: 10px;
&:hover {
color: $light-grey;
}
}
&_small {
font-size: 11px;
}
}

View File

@ -47,6 +47,7 @@ export default {
'NEW_TICKETS': 'New Tickets',
'ALL_TICKETS': 'All Tickets',
'CUSTOM_RESPONSES': 'Custom Responses',
'CUSTOM_TAGS': 'Custom Tags',
'LIST_USERS': 'List Users',
'BAN_USERS': 'Ban Users',
'LIST_ARTICLES': 'Article List',
@ -283,6 +284,7 @@ export default {
'ACCOUNT_DESCRIPTION': 'All your tickets are stored in your account\'s profile. Keep track of all your tickets you send to our staff team.',
'SUPPORT_CENTER_DESCRIPTION': 'Welcome to our support center. You can contact us through a tickets system. Your tickets will be answered by our staff.',
'CUSTOM_RESPONSES_DESCRIPTION': 'Custom responses are automated responses for common problems',
'CUSTOM_TAGS_DESCRIPTION': 'Here you can view manage the custom tags for tickets to identify them better',
'MY_TICKETS_DESCRIPTION': 'Here you can view the tickets you are responsible for.',
'NEW_TICKETS_DESCRIPTION': 'Here you can view all the new tickets that are not assigned by anyone.',
'ALL_TICKETS_DESCRIPTION': 'Here you can view the tickets of the departments you are assigned.',

View File

@ -62,6 +62,8 @@ class SessionStore {
this.setItem('allow-attachments', configs['allow-attachments']);
this.setItem('maintenance-mode', configs['maintenance-mode']);
this.setItem('max-size', configs['max-size']);
this.setItem('tags', configs['tags']);
}
getConfigs() {
@ -78,6 +80,7 @@ class SessionStore {
'allow-attachments': (this.getItem('allow-attachments') * 1),
'maintenance-mode': (this.getItem('maintenance-mode') * 1),
'max-size': this.getItem('max-size'),
'tags': this.getItem('tags')
};
}

View File

@ -16,7 +16,8 @@ use Respect\Validation\Validator as DataValidator;
* @apiParam {Number} name Name of the custom field.
* @apiParam {String} type One of 'text' and 'select'.
* @apiParam {String} options JSON array of strings with the option names.
*
* @apiParam {String} description Description of the custom field.
* @apiUse NO_PERMISSION
* @apiUse INVALID_NAME
* @apiUse INVALID_CUSTOM_FIELD_TYPE

View File

@ -56,7 +56,8 @@ class GetSettingsController extends Controller {
'supportedLanguages' => Language::getSupportedLanguages(),
'allowedLanguages' => Language::getAllowedLanguages(),
'session-prefix' => Setting::getSetting('session-prefix')->getValue(),
'mail-template-header-image' => Setting::getSetting('mail-template-header-image')->getValue()
'mail-template-header-image' => Setting::getSetting('mail-template-header-image')->getValue(),
'tags' => Tag::getAll()->toArray()
];
} else {
$settingsList = [
@ -73,7 +74,8 @@ class GetSettingsController extends Controller {
'supportedLanguages' => Language::getSupportedLanguages(),
'allowedLanguages' => Language::getAllowedLanguages(),
'user-system-enabled' => intval(Setting::getSetting('user-system-enabled')->getValue()),
'session-prefix' => Setting::getSetting('session-prefix')->getValue()
'session-prefix' => Setting::getSetting('session-prefix')->getValue(),
'tags' => Tag::getAll()->toArray()
];
}
}

View File

@ -16,5 +16,11 @@ $ticketControllers->addController(new ReOpenController);
$ticketControllers->addController(new ChangePriorityController);
$ticketControllers->addController(new SeenController);
$ticketControllers->addController(new DeleteController);
$ticketControllers->addController(new CreateTagController);
$ticketControllers->addController(new EditTagController);
$ticketControllers->addController(new DeleteTagController);
$ticketControllers->addController(new GetTagsController);
$ticketControllers->addController(new AddTagController);
$ticketControllers->addController(new RemoveTagController);
$ticketControllers->finalize();

View File

@ -0,0 +1,60 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/add-tag Add tag
* @apiVersion 4.3.2
*
* @apiName Add tag
*
* @apiGroup Ticket
*
* @apiDescription This path attaches a new tag to a ticket.
*
* @apiPermission staff1
*
* @apiParam {Number} ticketNumber The number of the ticket which the tag is going to be attached.
* @apiParam {String} tagId The id of the tag to attach.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TICKET
* @apiUse INVALID_TAG
*
* @apiSuccess {Object} data Empty object
*
*/
class AddTagController extends Controller {
const PATH = '/add-tag';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'ticketNumber' => [
'validation' => DataValidator::validTicketNumber(),
'error' => ERRORS::INVALID_TICKET
],
'tagId' => [
'validation' => DataValidator::dataStoreId('tag'),
'error' => ERRORS::INVALID_TAG
]
]
];
}
public function handler() {
$tagId = Controller::request('tagId');
$tag = Tag::getDataStore($tagId);
$ticket = Ticket::getByTicketNumber(Controller::request('ticketNumber'));
if ($ticket->sharedTagList->includesId($tagId)) throw new RequestException(ERRORS::TAG_EXISTS);
$ticket->sharedTagList->add($tag);
$ticket->store();
Response::respondSuccess();
}
}

View File

@ -0,0 +1,61 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/create-tag Create tag
* @apiVersion 4.3.2
*
* @apiName Create tag
*
* @apiGroup Ticket
*
* @apiDescription This path creates a new tag.
*
* @apiPermission staff1
*
* @apiParam {Number} name The new name of the tag.
* @apiParam {String} color The new color of the tag.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_NAME
* @apiUse TAG_EXISTS
*
* @apiSuccess {Object} data Empty object
*
*/
class CreateTagController extends Controller {
const PATH = '/create-tag';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'name' => [
'validation' => DataValidator::length(2, 100),
'error' => ERRORS::INVALID_NAME
]
]
];
}
public function handler() {
$name = Controller::request('name');
$color = Controller::request('color');
if (!Tag::getDataStore($name, 'name')->isNull()) {
throw new RequestException(ERRORS::TAG_EXISTS);
}
$tagInstance = new Tag();
$tagInstance->setProperties([
'name' => $name,
'color' => $color
]);
$tagInstance->store();
Response::respondSuccess();
}
}

View File

@ -0,0 +1,50 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/delete-tag Delete a tag
* @apiVersion 4.3.2
*
* @apiName Delete tag
*
* @apiGroup Ticket
*
* @apiDescription This path delete a tag.
*
* @apiPermission staff1
*
* @apiParam {Number} tagId The id of the tag.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TAG
*
* @apiSuccess {Object} data Empty object
*
*/
class DeleteTagController extends Controller {
const PATH = '/delete-tag';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'tagId' => [
'validation' => DataValidator::dataStoreId('tag'),
'error' => ERRORS::INVALID_TAG
]
]
];
}
public function handler() {
$tagInstance = Tag::getDataStore(Controller::request('tagId'));
$tagInstance->delete();
Response::respondSuccess();
}
}

View File

@ -0,0 +1,62 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/edit-tag Edit tag
* @apiVersion 4.3.2
*
* @apiName Edit tag
*
* @apiGroup Ticket
*
* @apiDescription This path edit tags.
*
* @apiPermission staff1
*
* @apiParam {Number} tagId The id of the tag.
* @apiParam {Number} name The new name of the tag.
* @apiParam {String} color The new color of the tag.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TAG
* @apiUse TAG_EXISTS
*
* @apiSuccess {Object} data Empty object
*
*/
class EditTagController extends Controller {
const PATH = '/edit-tag';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'tagId' => [
'validation' => DataValidator::dataStoreId('tag'),
'error' => ERRORS::INVALID_TAG
]
]
];
}
public function handler() {
$name = Controller::request('name');
$color = Controller::request('color');
$tagInstance = Tag::getDataStore(Controller::request('tagId'));
if($name) $tagInstance->name = $name;
if($color) $tagInstance->color = $color;
$newNameTagInstance = Tag::getDataStore($name, 'name');
if (!$newNameTagInstance ->isNull() && $newNameTagInstance->id !== $tagInstance->id) {
throw new RequestException(ERRORS::TAG_EXISTS);
}
$tagInstance->store();
Response::respondSuccess();
}
}

View File

@ -0,0 +1,39 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/get-tags Get tags
* @apiVersion 4.3.2
*
* @apiName Get tags
*
* @apiGroup Ticket
*
* @apiDescription This path returns all the tags.
*
* @apiPermission staff1
*
* @apiUse NO_PERMISSION
*
* @apiSuccess {Object} data Empty object
*
*/
class GetTagsController extends Controller {
const PATH = '/get-tags';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => []
];
}
public function handler() {
$tags = Tag::getAll();
Response::respondSuccess($tags->toArray());
}
}

View File

@ -0,0 +1,60 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/remove-tag Remove tag
* @apiVersion 4.3.2
*
* @apiName Remove tag
*
* @apiGroup Ticket
*
* @apiDescription This path removes a tag from a ticket.
*
* @apiPermission staff1
*
* @apiParam {Number} ticketNumber The number of the ticket which the tag is going to be removed.
* @apiParam {String} tagId The id of the tag to be removed.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_TICKET
* @apiUse INVALID_TAG
*
* @apiSuccess {Object} data Empty object
*
*/
class RemoveTagController extends Controller {
const PATH = '/remove-tag';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'staff_1',
'requestData' => [
'ticketNumber' => [
'validation' => DataValidator::validTicketNumber(),
'error' => ERRORS::INVALID_TICKET
],
'tagId' => [
'validation' => DataValidator::dataStoreId('tag'),
'error' => ERRORS::INVALID_TAG
]
]
];
}
public function handler() {
$tagId = Controller::request('tagId');
$tag = Tag::getDataStore($tagId);
$ticket = Ticket::getByTicketNumber(Controller::request('ticketNumber'));
if (!$ticket->sharedTagList->includesId($tagId)) throw new RequestException(ERRORS::INVALID_TAG);
$ticket->sharedTagList->remove($tag);
$ticket->store();
Response::respondSuccess();
}
}

View File

@ -11,7 +11,11 @@
* @apiDefine USER_EXISTS
* @apiError {String} USER_EXISTS The user already exists.
*/
/**
/**
* @apiDefine TAG_EXISTS
* @apiError {String} TAG_EXISTS The tag already exists.
*/
/**
* @apiDefine NO_PERMISSION
* @apiError {String} NO_PERMISSION You have no permission to perform this operation.
*/
@ -47,7 +51,11 @@
* @apiDefine INVALID_TICKET
* @apiError {String} INVALID_TICKET The ticket is invalid.
*/
/**
/**
* @apiDefine INVALID_TAG
* @apiError {String} INVALID_TAG The tag is invalid.
*/
/**
* @apiDefine INIT_SETTINGS_DONE
* @apiError {String} INIT_SETTINGS_DONE The init settings are already done.
*/
@ -240,6 +248,7 @@ class ERRORS {
const INVALID_CREDENTIALS = 'INVALID_CREDENTIALS';
const SESSION_EXISTS = 'SESSION_EXISTS';
const USER_EXISTS = 'USER_EXISTS';
const TAG_EXISTS = 'TAG_EXISTS';
const NO_PERMISSION = 'NO_PERMISSION';
const INVALID_TITLE = 'INVALID_TITLE';
const INVALID_CONTENT = 'INVALID_CONTENT';
@ -249,6 +258,7 @@ class ERRORS {
const INVALID_SETTING = 'INVALID_SETTING';
const INVALID_DEPARTMENT = 'INVALID_DEPARTMENT';
const INVALID_TICKET = 'INVALID_TICKET';
const INVALID_TAG = 'INVALID_TAG';
const INIT_SETTINGS_DONE = 'INIT_SETTINGS_DONE';
const INVALID_OLD_PASSWORD = 'INVALID_OLD_PASSWORD';
const INVALID_CAPTCHA = 'INVALID_CAPTCHA';

View File

@ -19,7 +19,7 @@ abstract class Controller {
$this->validate();
$this->handler();
} catch (\Exception $exception) {
Response::respondError($exception->getMessage(), $exception);
Response::respondError($exception->getMessage(), $exception, [$exception->__toString()]);
return;
}
};

View File

@ -43,6 +43,9 @@ class DataStoreId extends AbstractRule {
case 'customfield':
$dataStore = \Customfield::getDataStore($dataStoreId);
break;
case 'tag':
$dataStore = \Tag::getDataStore($dataStoreId);
break;
}
return !$dataStore->isNull();
@ -57,7 +60,8 @@ class DataStoreId extends AbstractRule {
'customresponse',
'topic',
'article',
'customfield'
'customfield',
'tag'
]);
}
}
}

25
server/models/Tag.php Normal file
View File

@ -0,0 +1,25 @@
<?php
//documentacion
class Tag extends DataStore {
const TABLE = 'tag';
public static function getProps() {
return [
'name',
'color'
];
}
public function toArray($minimized = false) {
if($minimized){
return $this->name;
} else {
return [
'id'=> $this->id,
'name'=> $this->name,
'color' => $this->color
];
}
}
}

View File

@ -49,7 +49,8 @@ class Ticket extends DataStore {
'unreadStaff',
'language',
'authorEmail',
'authorName'
'authorName',
'sharedTagList'
);
}
@ -128,7 +129,8 @@ class Ticket extends DataStore {
'priority' => $this->priority,
'author' => $this->authorToArray(),
'owner' => $this->ownerToArray(),
'events' => $minimized ? [] : $this->eventsToArray()
'events' => $minimized ? [] : $this->eventsToArray(),
'tags' => $this->sharedTagList->toArray(true)
];
}

View File

@ -54,7 +54,7 @@ require './system/add-department.rb'
require './system/edit-department.rb'
require './system/delete-department.rb'
require './staff/last-events.rb'
require './system/mail-templates.rb'
# require './system/mail-templates.rb'
require './system/disable-registration.rb'
require './system/enable-registration.rb'
require './system/add-api-key.rb'
@ -62,6 +62,9 @@ require './system/delete-api-key.rb'
require './system/get-api-keys.rb'
require './system/file-upload-download.rb'
require './system/csv-import.rb'
require './system/custom-fields.rb'
require './ticket/create-tag.rb'
require './ticket/edit-tag.rb'
require './ticket/get-tags.rb'
require './ticket/delete-tag.rb'
require './system/disable-user-system.rb'
require './system/get-stats.rb'

View File

@ -20,7 +20,7 @@ class Scripts
departments = request('/system/get-settings', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})['departments']
})['data']['departments']
departments = departments.collect { |x| x.id }
response = request('/staff/add', {

View File

@ -0,0 +1,64 @@
describe '/ticket/create-tag' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
it 'should add a tag' do
result = request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: 'tag1',
color: 'blue'
})
tag = $database.getRow('tag', 1 , 'id')
(result['status']).should.equal('success')
(tag['name']).should.equal('tag1')
(tag['color']).should.equal('blue')
end
it 'should not add tag if already exits' do
result = request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: 'tag1',
color: 'blue'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('TAG_EXISTS')
end
it 'should fail if the name is invalid' do
result = request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
color: 'black'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_NAME')
result = request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: 'T',
color: 'black'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_NAME')
long_text = ''
200.times {long_text << 'a'}
result = request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: long_text,
color: 'black'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_NAME')
end
end

View File

@ -0,0 +1,38 @@
describe '/ticket/delete-tag' do
it 'should fail if a user is logged' do
Scripts.login
result = request('/ticket/delete-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 1
})
(result['status']).should.equal('fail')
(result['message']).should.equal('NO_PERMISSION')
end
it 'should delete a tag if is a Staff 3 logged' do
Scripts.login($staff[:email], $staff[:password], true)
result = request('/ticket/delete-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 1
})
(result['status']).should.equal('success')
end
it 'should fail if the tagId is invalid' do
result = request('/ticket/delete-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 1000
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_TAG')
end
end

50
tests/ticket/edit-tag.rb Normal file
View File

@ -0,0 +1,50 @@
describe '/ticket/edit-tag' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
it 'should edit a tag' do
result = request('/ticket/edit-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 1,
name: 'TAG1',
color: 'yellow'
})
(result['status']).should.equal('success')
tag = $database.getRow('tag', 1, 'id')
(tag['name']).should.equal('TAG1')
(tag['color']).should.equal('yellow')
end
it 'should fail if the name already exists' do
request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: 'TAG2',
color: 'blue'
})
result = request('/ticket/edit-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 2,
name: 'TAG1'
})
(result['status']).should.equal('fail')
(result['message']).should.equal('TAG_EXISTS')
end
it 'should fail if the tagId is invalid' do
result = request('/ticket/edit-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
tagId: 100
})
(result['status']).should.equal('fail')
(result['message']).should.equal('INVALID_TAG')
end
end

38
tests/ticket/get-tags.rb Normal file
View File

@ -0,0 +1,38 @@
describe '/ticket/get-tags' do
it 'should fail if a user is logged' do
Scripts.login('steve@jobs.com', 'custompassword')
result = request('/ticket/get-tags', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('fail')
(result['message']).should.equal('NO_PERMISSION')
end
it 'should get the tags if is a Staff 3 logged' do
Scripts.login($staff[:email], $staff[:password], true)
request('/ticket/create-tag', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
name: 'TAG3',
color: 'grey'
})
result = request('/ticket/get-tags', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
(result['data'][0]['name']).should.equal('TAG1')
(result['data'][0]['color']).should.equal('yellow')
(result['data'][1]['name']).should.equal('TAG2')
(result['data'][1]['color']).should.equal('blue')
(result['data'][2]['name']).should.equal('TAG3')
(result['data'][2]['color']).should.equal('grey')
end
end

View File

@ -3,7 +3,7 @@ describe '/user/edit-email' do
request('/user/logout')
result = request('/user/login', {
email: 'steve@jobs.com',
password: 'newpassword'
password: 'custompassword'
})
$csrf_userid = result['data']['userId']
@ -35,5 +35,11 @@ describe '/user/edit-email' do
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
result = request('/user/edit-email', {
newEmail: 'steve@jobs.com',
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
end
end

View File

@ -3,7 +3,7 @@ describe '/user/edit-password' do
request('/user/logout')
result = request('/user/login', {
email: 'steve@jobs.com',
password: 'custom'
password: 'custompassword'
})
$csrf_userid = result['data']['userId']
@ -12,7 +12,7 @@ describe '/user/edit-password' do
it 'should fail if new password is incorrect' do
result = request('/user/edit-password', {
oldPassword: 'custom',
oldPassword: 'custompassword',
newPassword: 'np',
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
@ -24,7 +24,7 @@ describe '/user/edit-password' do
250.times {long_text << 'a'}
result = request('/user/edit-password', {
oldPassword: 'custom',
oldPassword: 'custompassword',
newPassword: long_text,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
@ -46,7 +46,7 @@ describe '/user/edit-password' do
it 'should change password' do
result = request('/user/edit-password',{
oldPassword: 'custom',
oldPassword: 'custompassword',
newPassword: 'newpassword',
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
@ -55,9 +55,13 @@ describe '/user/edit-password' do
request('/user/logout')
result = request('/user/login',{
email: 'steve@jobs.com',
password: 'newpassword'
Scripts.login('steve@jobs.com','newpassword')
result = request('/user/edit-password',{
oldPassword: 'newpassword',
newPassword: 'custompassword',
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
(result['status']).should.equal('success')
end

View File

@ -3,7 +3,7 @@ describe '/user/signup' do
response = request('/user/signup', {
:name => 'Steve Jobs',
:email => 'steve@jobs.com',
:password => 'custom'
:password => 'custompassword'
})
userRow = $database.getRow('user', response['data']['userId'])