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 DateTransformer from 'lib-core/date-transformer';
import Icon from 'core-components/icon'; import Icon from 'core-components/icon';
import Tooltip from 'core-components/tooltip'; 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 { class TicketEvent extends React.Component {
static propTypes = { static propTypes = {
@ -23,6 +28,13 @@ class TicketEvent extends React.Component {
content: React.PropTypes.string, content: React.PropTypes.string,
date: React.PropTypes.string, date: React.PropTypes.string,
private: 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() { render() {
@ -95,12 +107,38 @@ class TicketEvent extends React.Component {
{(this.props.private*1) ? this.renderPrivateBadge() : null} {(this.props.private*1) ? this.renderPrivateBadge() : null}
</div> </div>
<div className="ticket-event__comment-date">{DateTransformer.transformToString(this.props.date)}</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.props.edit ? this.renderContent() : this.renderEditField()}
{this.renderFileRow(this.props.file)} {this.renderFooter(this.props.file)}
</div> </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() { renderAssignment() {
let assignedTo = this.props.content; let assignedTo = this.props.content;
let authorName = this.props.author.name; let authorName = this.props.author.name;
@ -189,8 +227,9 @@ class TicketEvent extends React.Component {
); );
} }
renderFileRow(file) { renderFooter(file) {
let node = null; let node = null;
let edited = null;
if (file) { if (file) {
node = <span> {this.getFileLink(file)} <Icon name="paperclip" /> </span>; node = <span> {this.getFileLink(file)} <Icon name="paperclip" /> </span>;
@ -198,9 +237,18 @@ class TicketEvent extends React.Component {
node = i18n('NO_ATTACHMENT'); node = i18n('NO_ATTACHMENT');
} }
if (this.props.edited && this.props.type === 'COMMENT') {
edited = i18n('COMMENT_EDITED');
}
return ( return (
<div className="ticket-event__file"> <div className="ticket-event__items">
{node} <div className="ticket-event__edited">
{edited}
</div>
<div className="ticket-event__file">
{node}
</div>
</div> </div>
); );
} }

View File

@ -96,21 +96,47 @@
border-top: none; border-top: none;
padding: 20px 10px; padding: 20px 10px;
text-align: left; text-align: left;
position:relative;
img { img {
max-width:100%; 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; background-color: $very-light-grey;
cursor: pointer; display: flex;
text-align: right; align-items: center;
padding: 5px 10px; justify-content: space-between;
padding: 8px;
font-size: 12px; font-size: 12px;
} }
&__file {
cursor: pointer;
text-align: right;
}
&__edited{
font-style: italic;
}
&__comment-badge-value { &__comment-badge-value {
font-weight: normal; font-weight: normal;
} }

View File

@ -47,7 +47,8 @@ class TicketViewer extends React.Component {
ticket: { ticket: {
author: {}, author: {},
department: {}, department: {},
comments: [] comments: [],
edited: null
} }
}; };
@ -55,7 +56,9 @@ class TicketViewer extends React.Component {
loading: false, loading: false,
commentValue: TextEditor.createEmpty(), commentValue: TextEditor.createEmpty(),
commentEdited: false, commentEdited: false,
commentPrivate: false commentPrivate: false,
edit: false,
editId: 0
}; };
componentDidMount() { componentDidMount() {
@ -78,7 +81,21 @@ class TicketViewer extends React.Component {
</div> </div>
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()} {this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
<div className="ticket-viewer__content"> <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>
<div className="ticket-viewer__comments"> <div className="ticket-viewer__comments">
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))} {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') { if (this.props.userStaff && typeof options.content === 'string') {
options.content = MentionsParser.parse(options.content); options.content = MentionsParser.parse(options.content);
} }
return ( 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) { onSubmit(formState) {
this.setState({ this.setState({
loading: true loading: true

View File

@ -55,7 +55,7 @@ class AdminPanelCustomTags extends React.Component {
} }
renderTag(tag, index) { renderTag(tag, index) {
return( return (
<div key={index} className="admin-panel-custom-tags__tag-container" > <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 /> <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> </div>
@ -67,6 +67,7 @@ class AdminPanelCustomTags extends React.Component {
<AdminPanelCustomTagsModal onTagCreated={this.retrieveCustomTags.bind(this)} createTag /> <AdminPanelCustomTagsModal onTagCreated={this.retrieveCustomTags.bind(this)} createTag />
); );
} }
openEditTagModal(tagId,tagName,tagColor, event) { openEditTagModal(tagId,tagName,tagColor, event) {
ModalContainer.openModal( ModalContainer.openModal(
<AdminPanelCustomTagsModal defaultValues={{name: tagName , color: tagColor}} id={tagId} onTagChange={this.retrieveCustomTags.bind(this)}/> <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() { render() {
return( return (
this.props.createTag ? this.renderCreateTagContent() : this.renderEditTagContent() this.props.createTag ? this.renderCreateTagContent() : this.renderEditTagContent()
); );
} }
@ -88,6 +88,7 @@ class AdminPanelCustomTagsModal extends React.Component {
form form
}); });
} }
onSubmitEditTag(form) { onSubmitEditTag(form) {
this.setState({ this.setState({
loading: true loading: true
@ -120,6 +121,7 @@ class AdminPanelCustomTagsModal extends React.Component {
}); });
} }
onSubmitNewTag(form) { onSubmitNewTag(form) {
this.setState({ this.setState({
loading: true loading: true

View File

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

View File

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

View File

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

View File

@ -398,6 +398,7 @@ export default {
'SERVER_CREDENTIALS_WORKING': 'Server credentials are working correctly', '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?', '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_7_DAYS': 'Last 7 days',
'LAST_30_DAYS': 'Last 30 days', 'LAST_30_DAYS': 'Last 30 days',
'LAST_90_DAYS': 'Last 90 days', 'LAST_90_DAYS': 'Last 90 days',

View File

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

View File

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

View File

@ -48,7 +48,7 @@ class DeleteController extends Controller {
throw new RequestException(ERRORS::NO_PERMISSION); 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); 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', 'language',
'authorEmail', 'authorEmail',
'authorName', 'authorName',
'sharedTagList' 'sharedTagList',
'editedContent'
); );
} }
@ -130,7 +131,8 @@ class Ticket extends DataStore {
'author' => $this->authorToArray(), 'author' => $this->authorToArray(),
'owner' => $this->ownerToArray(), 'owner' => $this->ownerToArray(),
'events' => $minimized ? [] : $this->eventsToArray(), '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, 'date'=> $ticketEvent->date,
'file'=> $ticketEvent->file, 'file'=> $ticketEvent->file,
'private'=> $ticketEvent->private, 'private'=> $ticketEvent->private,
'edited' => $ticketEvent->editedContent,
'id' => $ticketEvent->id
]; ];
$author = $ticketEvent->getAuthor(); $author = $ticketEvent->getAuthor();

View File

@ -60,7 +60,8 @@ class Ticketevent extends DataStore {
'authorUser', 'authorUser',
'authorStaff', 'authorStaff',
'date', 'date',
'private' 'private',
'editedContent'
]; ];
} }
@ -75,6 +76,10 @@ class Ticketevent extends DataStore {
return new NullDataStore(); return new NullDataStore();
} }
public static function getTicketEvent($value, $property = 'id') {
return parent::getDataStore($value, $property);
}
public function toArray() { public function toArray() {
$user = ($this->authorStaff) ? $this->authorStaff : $this->authorUser; $user = ($this->authorStaff) ? $this->authorStaff : $this->authorUser;
@ -87,7 +92,8 @@ class Ticketevent extends DataStore {
'staff' => $user instanceOf Staff, 'staff' => $user instanceOf Staff,
'id' => $user ? $user->id : null, 'id' => $user ? $user->id : null,
'customfields' => $user->xownCustomfieldvalueList ? $user->xownCustomfieldvalueList->toArray() : [], '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/delete-tag.rb'
require './ticket/add-tag.rb' require './ticket/add-tag.rb'
require './ticket/delete-tag.rb' require './ticket/delete-tag.rb'
require './ticket/edit-comment.rb'
require './system/disable-user-system.rb' require './system/disable-user-system.rb'
# require './system/get-stats.rb' # require './system/get-stats.rb'

View File

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