edit comment ticket feature

This commit is contained in:
Guillermo 2019-06-26 22:04:56 -03:00
parent 37ab1c5817
commit 94a8cd4431
19 changed files with 366 additions and 36 deletions

View File

@ -7,6 +7,11 @@ import API from 'lib-app/api-call';
import DateTransformer from 'lib-core/date-transformer';
import Icon from 'core-components/icon';
import Tooltip from 'core-components/tooltip';
import TextEditor from 'core-components/text-editor';
import Button from 'core-components/button';
import SubmitButton from 'core-components/submit-button';
import Form from 'core-components/form';
import FormField from 'core-components/form-field';
class TicketEvent extends React.Component {
static propTypes = {
@ -23,6 +28,13 @@ class TicketEvent extends React.Component {
content: React.PropTypes.string,
date: React.PropTypes.string,
private: React.PropTypes.string,
edited: React.PropTypes.bool,
edit: React.PropTypes.bool,
onToggleEdit: React.PropTypes.func
};
state = {
content: this.props.content
};
render() {
@ -95,12 +107,38 @@ class TicketEvent extends React.Component {
{(this.props.private*1) ? this.renderPrivateBadge() : null}
</div>
<div className="ticket-event__comment-date">{DateTransformer.transformToString(this.props.date)}</div>
<div className="ticket-event__comment-content" dangerouslySetInnerHTML={{__html: this.props.content}}></div>
{this.renderFileRow(this.props.file)}
{!this.props.edit ? this.renderContent() : this.renderEditField()}
{this.renderFooter(this.props.file)}
</div>
);
}
renderContent() {
return (
<div className="ticket-event__comment-content">
<div dangerouslySetInnerHTML={{__html: this.props.content}}></div>
{((this.props.author.id === this.props.userId) || (this.props.userStaff)) ? this.renderEditIcon() : null}
</div>
)
}
renderEditIcon() {
return (
<div className="ticket-event__comment-content__edit" >
<Icon name="pencil" onClick={this.props.onToggleEdit} />
</div>
)
}
renderEditField() {
return (
<Form loading={this.props.loading} values={{content:this.state.content}} onChange={(form) => {this.setState({content:form.content})}} onSubmit={this.props.onEdit}>
<FormField name="content" required field="textarea" validation="TEXT_AREA" fieldProps={{allowImages: this.props.allowAttachments}}/>
<div className="ticket-event__submit-edited-comment" >
<SubmitButton type="secondary" >{i18n('SUBMIT')}</SubmitButton>
<Button size="medium" onClick={this.props.onToggleEdit}>{i18n('CLOSE')}</Button>
</div>
</Form>
);
}
renderAssignment() {
let assignedTo = this.props.content;
let authorName = this.props.author.name;
@ -189,8 +227,9 @@ class TicketEvent extends React.Component {
);
}
renderFileRow(file) {
renderFooter(file) {
let node = null;
let edited = null;
if (file) {
node = <span> {this.getFileLink(file)} <Icon name="paperclip" /> </span>;
@ -198,9 +237,18 @@ class TicketEvent extends React.Component {
node = i18n('NO_ATTACHMENT');
}
if (this.props.edited && this.props.type === 'COMMENT') {
edited = i18n('COMMENT_EDITED');
}
return (
<div className="ticket-event__file">
{node}
<div className="ticket-event__items">
<div className="ticket-event__edited">
{edited}
</div>
<div className="ticket-event__file">
{node}
</div>
</div>
);
}

View File

@ -96,21 +96,47 @@
border-top: none;
padding: 20px 10px;
text-align: left;
position:relative;
img {
max-width:100%;
}
&__edit {
position:absolute;
top: 3px;
right: 9px;
align-self: right;
color:white;
:hover {
color: grey;
cursor:pointer;
}
}
}
}
&__submit-edited-comment {
display:flex;
align-items: center;
justify-content: space-between;
padding: 8px;
}
&__file {
&__items {
background-color: $very-light-grey;
cursor: pointer;
text-align: right;
padding: 5px 10px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px;
font-size: 12px;
}
&__file {
cursor: pointer;
text-align: right;
}
&__edited{
font-style: italic;
}
&__comment-badge-value {
font-weight: normal;
}

View File

@ -47,7 +47,8 @@ class TicketViewer extends React.Component {
ticket: {
author: {},
department: {},
comments: []
comments: [],
edited: null
}
};
@ -55,7 +56,9 @@ class TicketViewer extends React.Component {
loading: false,
commentValue: TextEditor.createEmpty(),
commentEdited: false,
commentPrivate: false
commentPrivate: false,
edit: false,
editId: 0
};
componentDidMount() {
@ -78,7 +81,21 @@ class TicketViewer extends React.Component {
</div>
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
<div className="ticket-viewer__content">
<TicketEvent type="COMMENT" author={ticket.author} content={this.props.userStaff ? MentionsParser.parse(ticket.content) : ticket.content} date={ticket.date} file={ticket.file}/>
<TicketEvent
loading={this.state.loading}
type="COMMENT"
author={ticket.author}
content={this.props.userStaff ? MentionsParser.parse(ticket.content) : ticket.content}
userStaff={this.props.userStaff}
userId={this.props.userId}
date={ticket.date}
onEdit={this.onEdit.bind(this,0)}
edited={ticket.edited}
file={ticket.file}
edit={this.state.edit && this.state.editId == 0}
onToggleEdit={this.onToggleEdit.bind(this, 0)}
allowAttachments={this.props.allowAttachments}
/>
</div>
<div className="ticket-viewer__comments">
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))}
@ -218,9 +235,18 @@ class TicketViewer extends React.Component {
if (this.props.userStaff && typeof options.content === 'string') {
options.content = MentionsParser.parse(options.content);
}
return (
<TicketEvent {...options} author={(!_.isEmpty(options.author)) ? options.author : this.props.ticket.author} key={index} />
<TicketEvent
{...options}
author={(!_.isEmpty(options.author)) ? options.author : this.props.ticket.author}
userStaff={this.props.userStaff}
userId={this.props.userId}
onEdit={this.onEdit.bind(this, options.id)}
edit={this.state.edit && this.state.editId == options.id}
onToggleEdit={this.onToggleEdit.bind(this, options.id)}
key={index}
allowAttachments={this.props.allowAttachments}
/>
);
}
@ -452,6 +478,51 @@ class TicketViewer extends React.Component {
}
}
onToggleEdit(ticketEventId){
this.setState({
edit: !this.state.edit,
editId: ticketEventId
})
}
onEdit(ticketeventid,{content}) {
this.setState({
loading: true
});
const data = {};
if(ticketeventid){
data.ticketeventId = ticketeventid
}else{
data.ticketNumber = this.props.ticket.ticketNumber
}
API.call({
path: '/ticket/edit-comment',
data: _.extend(
data,
TextEditor.getContentFormData(content)
)
}).then(this.onEditCommentSuccess.bind(this), this.onFailCommentFail.bind(this));
}
onEditCommentSuccess() {
this.setState({
loading: false,
commentError: false,
commentEdited: false,
edit:false
});
this.onTicketModification();
}
onFailCommentFail() {
this.setState({
loading: false,
commentError: true
});
}
onSubmit(formState) {
this.setState({
loading: true

View File

@ -55,7 +55,7 @@ class AdminPanelCustomTags extends React.Component {
}
renderTag(tag, index) {
return(
return (
<div key={index} className="admin-panel-custom-tags__tag-container" >
<Tag color={tag.color} name={tag.name} onEditClick={this.openEditTagModal.bind(this, tag.id, tag.name, tag.color)} onRemoveClick={this.onDeleteClick.bind(this, tag.id)} size='large' showEditButton showDeleteButton />
</div>
@ -67,6 +67,7 @@ class AdminPanelCustomTags extends React.Component {
<AdminPanelCustomTagsModal onTagCreated={this.retrieveCustomTags.bind(this)} createTag />
);
}
openEditTagModal(tagId,tagName,tagColor, event) {
ModalContainer.openModal(
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)}/>

View File

@ -26,7 +26,7 @@ class AdminPanelCustomTagsModal extends React.Component {
};
render() {
return(
return (
this.props.createTag ? this.renderCreateTagContent() : this.renderEditTagContent()
);
}
@ -88,6 +88,7 @@ class AdminPanelCustomTagsModal extends React.Component {
form
});
}
onSubmitEditTag(form) {
this.setState({
loading: true
@ -120,6 +121,7 @@ class AdminPanelCustomTagsModal extends React.Component {
});
}
onSubmitNewTag(form) {
this.setState({
loading: true

View File

@ -55,7 +55,7 @@ class AdminPanelCustomTags extends React.Component {
}
renderTag(tag, index) {
return(
return (
<div key={index} className="admin-panel-custom-tags__tag-container" >
<Tag color={tag.color} name={tag.name} onRemoveClick={this.onDeleteClick.bind(this, tag.id)} size='large' showDeleteButton />
</div>

View File

@ -15,7 +15,8 @@ class Form extends React.Component {
onValidateErrors: React.PropTypes.func,
onChange: React.PropTypes.func,
values: React.PropTypes.object,
onSubmit: React.PropTypes.func
onSubmit: React.PropTypes.func,
defaultValues: React.PropTypes.object
};
static childContextTypes = {
@ -24,9 +25,8 @@ class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
form: {},
form: props.defaultValues || {},
validations: {},
errors: {}
};

View File

@ -45,7 +45,7 @@ class TextEditor extends React.Component {
}
state = {
value: '',
value: this.props.value,
focused: false
};

View File

@ -398,6 +398,7 @@ export default {
'SERVER_CREDENTIALS_WORKING': 'Server credentials are working correctly',
'DELETE_CUSTOM_FIELD_SURE': 'Some users may be using this field. Are you sure you want to delete it?',
'COMMENT_EDITED': '(comment edited)',
'LAST_7_DAYS': 'Last 7 days',
'LAST_30_DAYS': 'Last 30 days',
'LAST_90_DAYS': 'Last 90 days',

View File

@ -52,18 +52,18 @@ class DeleteStaffController extends Controller {
foreach($staff->sharedTicketList as $ticket) {
$ticket->owner = null;
$ticket->true = true;
$ticket->unreadStaff = true;
$ticket->store();
}
foreach($staff->sharedDepartmentList as $department) {
$department->owners--;
$department->store();
}
RedBean::exec('DELETE FROM log WHERE author_staff_id = ?', [$staffId]);
$staff->delete();
Response::respondSuccess();
}
}
}

View File

@ -3,6 +3,7 @@ $ticketControllers = new ControllerGroup();
$ticketControllers->setGroupPath('/ticket');
$ticketControllers->addController(new CreateController);
$ticketControllers->addController(new EditCommentController);
$ticketControllers->addController(new CommentController);
$ticketControllers->addController(new TicketGetController);
$ticketControllers->addController(new CheckTicketController);

View File

@ -48,7 +48,7 @@ class DeleteController extends Controller {
throw new RequestException(ERRORS::NO_PERMISSION);
}
if(Controller::isStaffLogged() && $user->level < 3) {
if(Controller::isStaffLogged() && $user->level < 3 && ($user->email !== $ticketAuthor['email'])) {
throw new RequestException(ERRORS::NO_PERMISSION);
}

View File

@ -0,0 +1,67 @@
<?php
use Respect\Validation\Validator as DataValidator;
DataValidator::with('CustomValidations', true);
/**
* @api {post} /ticket/edit-comment Edit a comment
* @apiVersion 4.4.0
*
* @apiName Edit comment
*
* @apiGroup Ticket
*
* @apiDescription This path edit a comment.
*
* @apiPermission user
*
* @apiParam {String} content The new content of the comment.
* @apiParam {Number} ticketEventId The id of the ticket event.
* @apiParam {Number} ticketNumber The id of the ticket number.
*
* @apiUse NO_PERMISSION
* @apiUse INVALID_CONENT
*
* @apiSuccess {Object} data Empty object
*
*/
class EditCommentController extends Controller {
const PATH = '/edit-comment';
const METHOD = 'POST';
public function validations() {
return [
'permission' => 'user',
'requestData' => [
'content' => [
'validation' => DataValidator::length(10, 5000),
'error' => ERRORS::INVALID_CONTENT
]
]
];
}
public function handler() {
$user = Controller::getLoggedUser();
$newcontent = Controller::request('content');
$ticketevent = Ticketevent::getTicketEvent(Controller::request('ticketEventId'));
$ticket = Ticket::getByTicketNumber(Controller::request('ticketNumber'));
if(!Controller::isStaffLogged() && ($user->id !== $ticketevent->authorUserId && $user->id !== $ticket->authorId )){
throw new RequestException(ERRORS::NO_PERMISSION);
}
if(!$ticketevent->isNull()){
$ticketevent->content = $newcontent;
$ticketevent->editedContent = true;
$ticketevent->store();
}else{
$ticket->content = $newcontent;
$ticket->editedContent = true;
$ticket->store();
}
Response::respondSuccess();
}
}

View File

@ -50,7 +50,8 @@ class Ticket extends DataStore {
'language',
'authorEmail',
'authorName',
'sharedTagList'
'sharedTagList',
'editedContent'
);
}
@ -130,7 +131,8 @@ class Ticket extends DataStore {
'author' => $this->authorToArray(),
'owner' => $this->ownerToArray(),
'events' => $minimized ? [] : $this->eventsToArray(),
'tags' => $this->sharedTagList->toArray(true)
'tags' => $this->sharedTagList->toArray(true),
'edited' => $this->editedContent
];
}
@ -181,6 +183,8 @@ class Ticket extends DataStore {
'date'=> $ticketEvent->date,
'file'=> $ticketEvent->file,
'private'=> $ticketEvent->private,
'edited' => $ticketEvent->editedContent,
'id' => $ticketEvent->id
];
$author = $ticketEvent->getAuthor();

View File

@ -60,7 +60,8 @@ class Ticketevent extends DataStore {
'authorUser',
'authorStaff',
'date',
'private'
'private',
'editedContent'
];
}
@ -75,6 +76,10 @@ class Ticketevent extends DataStore {
return new NullDataStore();
}
public static function getTicketEvent($value, $property = 'id') {
return parent::getDataStore($value, $property);
}
public function toArray() {
$user = ($this->authorStaff) ? $this->authorStaff : $this->authorUser;
@ -87,7 +92,8 @@ class Ticketevent extends DataStore {
'staff' => $user instanceOf Staff,
'id' => $user ? $user->id : null,
'customfields' => $user->xownCustomfieldvalueList ? $user->xownCustomfieldvalueList->toArray() : [],
]
],
'edited' => $this->editedContent
];
}
}

View File

@ -68,5 +68,6 @@ require './ticket/get-tags.rb'
require './ticket/delete-tag.rb'
require './ticket/add-tag.rb'
require './ticket/delete-tag.rb'
require './ticket/edit-comment.rb'
require './system/disable-user-system.rb'
# require './system/get-stats.rb'

View File

@ -67,10 +67,10 @@ class Scripts
request('/user/logout')
end
def self.createTicket(title = 'Winter is coming')
def self.createTicket(title = 'Winter is coming',content = 'The north remembers')
result = request('/ticket/create', {
title: title,
content: 'The north remembers',
content: content,
departmentId: 1,
language: 'en',
csrf_userid: $csrf_userid,
@ -115,4 +115,12 @@ class Scripts
color: color
})
end
def self.commentTicket(ticketnumber,content)
request('/ticket/comment', {
content: content,
ticketNumber: ticketnumber,
csrf_userid: $csrf_userid,
csrf_token: $csrf_token
})
end
end

View File

@ -19,7 +19,7 @@ describe'system/disable-user-system' do
numberOftickets= $database.query("SELECT * FROM ticket WHERE author_id IS NULL AND author_email IS NOT NULL AND author_name IS NOT NULL")
(numberOftickets.num_rows).should.equal(40)
(numberOftickets.num_rows).should.equal(41)
request('/user/logout')
@ -127,7 +127,7 @@ describe'system/disable-user-system' do
numberOftickets= $database.query("SELECT * FROM ticket WHERE author_email IS NULL AND author_name IS NULL AND author_id IS NOT NULL" )
(numberOftickets.num_rows).should.equal(41)
(numberOftickets.num_rows).should.equal(42)
end

View File

@ -0,0 +1,94 @@
describe '/ticket/edit-comment' do
request('/user/logout')
Scripts.login();
Scripts.createTicket('ticket made by an user','content of the ticket made by an user')
ticket = $database.getRow('ticket', 'ticket made by an user', 'title')
Scripts.commentTicket(ticket['ticket_number'],'com ment of a user')
it 'should change content of the ticket if the author user tries it' do
result = request('/ticket/edit-comment', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
content: 'content edited by the user',
ticketNumber: ticket['ticket_number']
})
ticket = $database.getRow('ticket', 'ticket made by an user', 'title')
(result['status']).should.equal('success')
(ticket['content']).should.equal('content edited by the user')
end
it 'should change the content of a comment if the user is the author' do
ticketevent = $database.getRow('ticketevent', 'com ment of a user', 'content')
result = request('/ticket/edit-comment', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
content: 'comment edited by the user',
ticketEventId: ticketevent['id']
})
ticketevent = $database.getRow('ticketevent', 'comment edited by the user', 'content')
(result['status']).should.equal('success')
(ticketevent['content']).should.equal('comment edited by the user')
end
it 'should change the content of a comment and the content of the ticket if the admin is logged' do
request('/user/logout')
Scripts.login($staff[:email], $staff[:password], true)
ticketevent = $database.getRow('ticketevent', 'comment edited by the user', 'content')
result = request('/ticket/edit-comment', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
content: 'comment edited by a staff',
ticketEventId: ticketevent['id']
})
ticketevent = $database.getRow('ticketevent', 'comment edited by a staff', 'content')
(result['status']).should.equal('success')
(ticketevent['content']).should.equal('comment edited by a staff')
result = request('/ticket/edit-comment', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
content: 'content edited by a staff',
ticketNumber: ticket['ticket_number']
})
ticket = $database.getRow('ticket', ticket['ticket_number'], 'ticket_number')
(result['status']).should.equal('success')
(ticket['content']).should.equal('content edited by a staff')
request('/user/logout')
end
it 'should not change the content of a comment if the user is not the author' do
Scripts.login($staff[:email], $staff[:password], true)
ticket = $database.getRow('ticket', 'ticket made by an user', 'title')
Scripts.commentTicket(ticket['ticket_number'],'comment by a staffffff')
ticketevent = $database.getRow('ticketevent', 'comment by a staffffff', 'content')
request('/user/logout')
Scripts.login();
result = request('/ticket/edit-comment', {
csrf_userid: $csrf_userid,
csrf_token: $csrf_token,
content: 'comment edited by a user',
ticketEventId: ticketevent['id']
})
(result['status']).should.equal('fail')
(result['message']).should.equal('NO_PERMISSION')
end
end