From 94a8cd4431a5e509d00c40b7a64cd068da222cdb Mon Sep 17 00:00:00 2001 From: Guillermo Date: Wed, 26 Jun 2019 22:04:56 -0300 Subject: [PATCH] edit comment ticket feature --- client/src/app-components/ticket-event.js | 58 +++++++++++- client/src/app-components/ticket-event.scss | 36 ++++++- client/src/app-components/ticket-viewer.js | 81 +++++++++++++++- .../panel/settings/admin-panel-custom-tags.js | 3 +- .../tickets/admin-panel-custom-tags-modal.js | 4 +- .../panel/tickets/admin-panel-custom-tags.js | 2 +- client/src/core-components/form.js | 6 +- client/src/core-components/text-editor.js | 2 +- client/src/data/languages/en.js | 1 + server/controllers/staff/delete.php | 10 +- server/controllers/ticket.php | 1 + server/controllers/ticket/delete.php | 2 +- server/controllers/ticket/edit-comment.php | 67 +++++++++++++ server/models/Ticket.php | 8 +- server/models/Ticketevent.php | 10 +- tests/init.rb | 1 + tests/scripts.rb | 12 ++- tests/system/disable-user-system.rb | 4 +- tests/ticket/edit-comment.rb | 94 +++++++++++++++++++ 19 files changed, 366 insertions(+), 36 deletions(-) create mode 100644 server/controllers/ticket/edit-comment.php create mode 100644 tests/ticket/edit-comment.rb diff --git a/client/src/app-components/ticket-event.js b/client/src/app-components/ticket-event.js index 06a241d4..375f6391 100644 --- a/client/src/app-components/ticket-event.js +++ b/client/src/app-components/ticket-event.js @@ -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}
{DateTransformer.transformToString(this.props.date)}
-
- {this.renderFileRow(this.props.file)} + {!this.props.edit ? this.renderContent() : this.renderEditField()} + {this.renderFooter(this.props.file)} ); } + renderContent() { + return ( +
+
+ {((this.props.author.id === this.props.userId) || (this.props.userStaff)) ? this.renderEditIcon() : null} +
+ ) + } + renderEditIcon() { + return ( +
+ +
+ ) + } + renderEditField() { + return ( +
{this.setState({content:form.content})}} onSubmit={this.props.onEdit}> + +
+ {i18n('SUBMIT')} + +
+ + ); + } 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 = {this.getFileLink(file)} ; @@ -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 ( -
- {node} +
+
+ {edited} +
+
+ {node} +
); } diff --git a/client/src/app-components/ticket-event.scss b/client/src/app-components/ticket-event.scss index 26ccbf89..c5b6e920 100644 --- a/client/src/app-components/ticket-event.scss +++ b/client/src/app-components/ticket-event.scss @@ -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; } diff --git a/client/src/app-components/ticket-viewer.js b/client/src/app-components/ticket-viewer.js index fd724874..09e1fbad 100644 --- a/client/src/app-components/ticket-viewer.js +++ b/client/src/app-components/ticket-viewer.js @@ -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 {
{this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()}
- +
{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 ( - + ); } @@ -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 diff --git a/client/src/app/admin/panel/settings/admin-panel-custom-tags.js b/client/src/app/admin/panel/settings/admin-panel-custom-tags.js index 06121f2d..c74c5bb3 100644 --- a/client/src/app/admin/panel/settings/admin-panel-custom-tags.js +++ b/client/src/app/admin/panel/settings/admin-panel-custom-tags.js @@ -55,7 +55,7 @@ class AdminPanelCustomTags extends React.Component { } renderTag(tag, index) { - return( + return (
@@ -67,6 +67,7 @@ class AdminPanelCustomTags extends React.Component { ); } + openEditTagModal(tagId,tagName,tagColor, event) { ModalContainer.openModal( diff --git a/client/src/app/admin/panel/tickets/admin-panel-custom-tags-modal.js b/client/src/app/admin/panel/tickets/admin-panel-custom-tags-modal.js index 233d4174..9db3e6ed 100644 --- a/client/src/app/admin/panel/tickets/admin-panel-custom-tags-modal.js +++ b/client/src/app/admin/panel/tickets/admin-panel-custom-tags-modal.js @@ -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 diff --git a/client/src/app/admin/panel/tickets/admin-panel-custom-tags.js b/client/src/app/admin/panel/tickets/admin-panel-custom-tags.js index d72f1e8b..79676a70 100644 --- a/client/src/app/admin/panel/tickets/admin-panel-custom-tags.js +++ b/client/src/app/admin/panel/tickets/admin-panel-custom-tags.js @@ -55,7 +55,7 @@ class AdminPanelCustomTags extends React.Component { } renderTag(tag, index) { - return( + return (
diff --git a/client/src/core-components/form.js b/client/src/core-components/form.js index ea422d54..d6de43af 100644 --- a/client/src/core-components/form.js +++ b/client/src/core-components/form.js @@ -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: {} }; diff --git a/client/src/core-components/text-editor.js b/client/src/core-components/text-editor.js index 3967ef8e..f3a1a733 100644 --- a/client/src/core-components/text-editor.js +++ b/client/src/core-components/text-editor.js @@ -45,7 +45,7 @@ class TextEditor extends React.Component { } state = { - value: '', + value: this.props.value, focused: false }; diff --git a/client/src/data/languages/en.js b/client/src/data/languages/en.js index c691d35f..6e6781de 100644 --- a/client/src/data/languages/en.js +++ b/client/src/data/languages/en.js @@ -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', diff --git a/server/controllers/staff/delete.php b/server/controllers/staff/delete.php index 70ff34fe..fd51f58b 100755 --- a/server/controllers/staff/delete.php +++ b/server/controllers/staff/delete.php @@ -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(); } - -} \ No newline at end of file + +} diff --git a/server/controllers/ticket.php b/server/controllers/ticket.php index 948f3221..5afbf8d5 100755 --- a/server/controllers/ticket.php +++ b/server/controllers/ticket.php @@ -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); diff --git a/server/controllers/ticket/delete.php b/server/controllers/ticket/delete.php index 7e447f99..1f8b094a 100644 --- a/server/controllers/ticket/delete.php +++ b/server/controllers/ticket/delete.php @@ -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); } diff --git a/server/controllers/ticket/edit-comment.php b/server/controllers/ticket/edit-comment.php new file mode 100644 index 00000000..3259ee0c --- /dev/null +++ b/server/controllers/ticket/edit-comment.php @@ -0,0 +1,67 @@ + '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(); + } +} diff --git a/server/models/Ticket.php b/server/models/Ticket.php index 6e7c3148..93fcd13d 100755 --- a/server/models/Ticket.php +++ b/server/models/Ticket.php @@ -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(); diff --git a/server/models/Ticketevent.php b/server/models/Ticketevent.php index e62f7152..c2880455 100755 --- a/server/models/Ticketevent.php +++ b/server/models/Ticketevent.php @@ -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 ]; } } diff --git a/tests/init.rb b/tests/init.rb index 2002848b..5274d26f 100644 --- a/tests/init.rb +++ b/tests/init.rb @@ -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' diff --git a/tests/scripts.rb b/tests/scripts.rb index 309d6bba..ff92b8d5 100644 --- a/tests/scripts.rb +++ b/tests/scripts.rb @@ -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 diff --git a/tests/system/disable-user-system.rb b/tests/system/disable-user-system.rb index 703a71b4..3a0013ba 100644 --- a/tests/system/disable-user-system.rb +++ b/tests/system/disable-user-system.rb @@ -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 diff --git a/tests/ticket/edit-comment.rb b/tests/ticket/edit-comment.rb new file mode 100644 index 00000000..f0df57c4 --- /dev/null +++ b/tests/ticket/edit-comment.rb @@ -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