From 938e25b2faddbb8fc680b7555ae89a92fcebd744 Mon Sep 17 00:00:00 2001 From: LautaroCesso <59095036+LautaroCesso@users.noreply.github.com> Date: Thu, 30 Jul 2020 22:30:48 -0300 Subject: [PATCH] Add edit buttons for fields in ticket viewer (#835) * Fix style color in autocomplete component and dropdown in ticket search and ticket viewer. * Change style of ticket status edit in ticket viewer. * Redirect to ticket search when author name was clicked in ticket viewer * Add i18n ASSIGNED_TO_ME. * Add content on selected in dropdown component. * Add styles in edit tags in ticket viewers * Add edit owner and delete i18n ASSIGNED_TO_ME. * Redirect to ticket search when owner name was clicked in ticket viewer * Add style in edit department in ticket viewer. * Add nav links in owner and author in ticket viewer. --- .../app-components/ticket-query-filters.js | 2 +- .../app-components/ticket-query-filters.scss | 9 + client/src/app-components/ticket-viewer.js | 386 +++++++++++++----- client/src/app-components/ticket-viewer.scss | 45 +- client/src/core-components/autocomplete.scss | 4 +- client/src/core-components/drop-down.js | 4 +- 6 files changed, 339 insertions(+), 111 deletions(-) diff --git a/client/src/app-components/ticket-query-filters.js b/client/src/app-components/ticket-query-filters.js index 6e8f91fb..254c5229 100644 --- a/client/src/app-components/ticket-query-filters.js +++ b/client/src/app-components/ticket-query-filters.js @@ -63,7 +63,7 @@ class TicketQueryFilters extends React.Component {
{i18n('STATUS')} - +
diff --git a/client/src/app-components/ticket-query-filters.scss b/client/src/app-components/ticket-query-filters.scss index 9e8926f0..49d1b53f 100644 --- a/client/src/app-components/ticket-query-filters.scss +++ b/client/src/app-components/ticket-query-filters.scss @@ -17,6 +17,7 @@ flex-direction: row; align-items: center; justify-content: flex-end; + &__button { margin: 0 10px; } @@ -52,6 +53,14 @@ flex-direction: column; align-items: start; margin-bottom: 30px; + + &__status-drop-down > .drop-down__current-item { + background-color: $very-light-grey; + + &:focus { + background-color: $medium-grey; + } + } } } diff --git a/client/src/app-components/ticket-viewer.js b/client/src/app-components/ticket-viewer.js index c7eec937..1294a2d4 100644 --- a/client/src/app-components/ticket-viewer.js +++ b/client/src/app-components/ticket-viewer.js @@ -9,6 +9,7 @@ import API from 'lib-app/api-call'; import SessionStore from 'lib-app/session-store'; import MentionsParser from 'lib-app/mentions-parser'; import history from 'lib-app/history'; +import searchTicketsUtils from 'lib-app/search-tickets-utils'; import TicketEvent from 'app-components/ticket-event'; import AreYouSure from 'app-components/are-you-sure'; @@ -66,41 +67,58 @@ class TicketViewer extends React.Component { newTitle: this.props.ticket.title, editTitleError: false, editTitleLoading: false, + editStatus: false, + editTags: false, + editOwner: false, + editDepartment: false, }; componentDidMount() { - if(!this.props.staffMembersLoaded && this.props.userStaff) { - this.props.dispatch(AdminDataActions.retrieveStaffMembers()); + const { + staffMembersLoaded, + userStaff, + dispatch + } = this.props; + + if(!staffMembersLoaded && userStaff) { + dispatch(AdminDataActions.retrieveStaffMembers()); } } render() { - const ticket = this.props.ticket; + const { + ticket, + userStaff, + userId, + editable, + allowAttachments, + assignmentAllowed + } = this.props; return (
{this.state.editTitle ? this.renderEditableTitle() : this.renderTitleHeader()} - {this.props.editable ? this.renderEditableHeaders() : this.renderHeaders()} + {editable ? this.renderEditableHeaders() : this.renderHeaders()}
+ allowAttachments={allowAttachments} />
{ticket.events && ticket.events.map(this.renderTicketEvent.bind(this))}
- {(!this.props.ticket.closed && (this.props.editable || !this.props.assignmentAllowed)) ? this.renderResponseField() : (this.showDeleteButton())? : null} + {(!ticket.closed && (editable || !assignmentAllowed)) ? this.renderResponseField() : (this.showDeleteButton())? : null}
); } @@ -114,7 +132,7 @@ class TicketViewer extends React.Component { #{ticketNumber} {title} - + {((author.id == userId && author.staff == userStaff) || userStaff) ? this.renderEditTitleOption() : null} {editedTitle ? this.renderEditedTitleText() : null } @@ -122,20 +140,6 @@ class TicketViewer extends React.Component { ) } - renderEditedTitleText(){ - return( -
{i18n('TITLE_EDITED')}
- ) - } - - renderEditTitleOption() { - return( - - this.setState({editTitle: true})} /> - - ) - } - renderEditableTitle(){ return(
@@ -160,59 +164,114 @@ class TicketViewer extends React.Component { } renderEditableHeaders() { - const ticket = this.props.ticket; - const departments = this.getDepartmentsForTransfer(); + const { userStaff, ticket } = this.props; + const filtersOnlyWithAuthor = { + authors: [ + { + id: ticket.author.id*1, + isStaff: ticket.author.staff*1 + } + ] + }; return (
-
{i18n('DEPARTMENT')}
-
- +
+
{i18n('DEPARTMENT')}
+
+ { + this.state.editDepartment ? + this.renderEditDepartment() : + ticket.department.name + } +
+ {userStaff ? this.renderEditOption("Department") : null}
-
{i18n('TAGS')}
-
- +
+
{i18n('TAGS')}
+
+ { + this.state.editTags ? + this.renderEditTags() : + this.renderTags() + } +
+ {userStaff ? this.renderEditOption("Tags") : null}
-
{i18n('OWNER')}
-
- {this.renderAssignStaffList()} +
+
{i18n('OWNER')}
+
+ {this.renderOwnerNode()} +
+ {userStaff ? this.renderEditOption("Owner") : null}
-
-
{i18n('AUTHOR')}
-
{ticket.author.name}
-
-
{i18n('STATUS')}
+
+
{i18n('AUTHOR')}
- {ticket.closed ? - : i18n('OPENED')} + + {ticket.author.name} + +
+
+
+
{i18n('STATUS')}
+
+ {this.state.editStatus ? this.renderEditStatus() : (ticket.closed ? i18n('CLOSED') : i18n('OPENED'))} +
+
+ {userStaff ? this.renderEditOption("Status") : null} +
); } + renderEditTags() { + const { + tags, + ticket + } = this.props; + + return ( +
+ + {this.renderCancelButton("Tags")} +
+ ); + } + + renderEditStatus() { + return ( +
+ {this.renderCancelButton("Status")} + {this.props.ticket.closed ? + : + } +
+ ); + } + renderHeaders() { const ticket = this.props.ticket; @@ -229,10 +288,9 @@ class TicketViewer extends React.Component {
{i18n('TAGS')}
-
{ticket.tags.length ? ticket.tags.map((tagName,index) => { - let tag = _.find(this.props.tags, {name:tagName}); - return - }) : i18n('NONE')}
+
+ {this.renderTags()} +
@@ -245,7 +303,7 @@ class TicketViewer extends React.Component {
{i18n('STATUS')}
- {i18n((this.props.ticket.closed) ? 'CLOSED' : 'OPENED')} + {i18n((ticket.closed) ? 'CLOSED' : 'OPENED')}
@@ -253,53 +311,137 @@ class TicketViewer extends React.Component { ); } - renderOwnerNode() { - let ownerNode = null; + renderTags() { + const { + ticket, + tags + } = this.props; + const TAGS = ( + ticket.tags.length ? + ticket.tags.map((tagName, index) => { + const tag = _.find(tags, {name: tagName}); + return + }) : + i18n('NONE') + ); - if (this.props.assignmentAllowed) { - ownerNode = this.renderAssignStaffList(); - } else { - ownerNode = (this.props.ticket.owner) ? this.props.ticket.owner.name : i18n('NONE') - } - - return ownerNode; + return TAGS; } - renderAssignStaffList() { + renderOwnerNode() { + const { + assignmentAllowed, + ticket + } = this.props; + const filtersOnlyWithOwner = ticket.owner && {owners: [ticket.owner.id*1]}; + let ownerNode = null; + + if(assignmentAllowed && ticket.owner) { + ownerNode = ( + + {ticket.owner.name} + + ); + } else { + ownerNode = ( + + {(ticket.owner) ? ticket.owner.name : i18n('NONE')} + + ); + } + + return (assignmentAllowed && this.state.editOwner) ? this.renderEditOwner() : ownerNode; + } + + renderEditOwner() { const items = this.getStaffAssignmentItems(); - const ownerId = this.props.ticket.owner && this.props.ticket.owner.id*1; + const { ticket } = this.props; + const ownerId = ticket.owner && ticket.owner.id*1; let selectedIndex = _.findIndex(items, {id: ownerId}); selectedIndex = (selectedIndex !== -1) ? selectedIndex : 0; return ( - +
+ + {this.renderCancelButton("Owner")} +
); } + renderEditDepartment() { + const { ticket } = this.props; + const departments = this.getDepartmentsForTransfer(); + + return ( +
+ + {this.renderCancelButton("Department")} +
+ ); + } + + renderEditTitleOption() { + return( + + this.setState({editTitle: true})} /> + + ) + } + + renderEditOption(option) { + return( + + this.setState({["edit"+option]: true})} /> + + ); + } + + renderEditedTitleText(){ + return( +
{i18n('TITLE_EDITED')}
+ ) + } + + renderCancelButton(option) { + return + } + renderTicketEvent(options, index) { - if (this.props.userStaff && typeof options.content === 'string') { + const { + userStaff, + ticket, + userId, + allowAttachments + } = this.props; + + if(userStaff && typeof options.content === 'string') { options.content = MentionsParser.parse(options.content); } + return ( + allowAttachments={allowAttachments} /> ); } renderResponseField() { + const { allowAttachments } = this.props; + return (
@@ -311,12 +453,11 @@ class TicketViewer extends React.Component {
- - {(this.props.allowAttachments) ? : null} + + {allowAttachments ? : null}
{i18n('RESPOND_TICKET')}
- {(this.showDeleteButton())? : null}
@@ -330,7 +471,7 @@ class TicketViewer extends React.Component { renderCustomResponses() { let customResponsesNode = null; - if (this.props.customResponses && this.props.editable) { + if(this.props.customResponses && this.props.editable) { let customResponses = this.props.customResponses.map((customResponse) => { return { content: customResponse.name @@ -343,7 +484,7 @@ class TicketViewer extends React.Component { customResponsesNode = (
- +
); } @@ -352,10 +493,10 @@ class TicketViewer extends React.Component { } renderPrivate() { - if (this.props.userStaff) { + if(this.props.userStaff) { return (
- +
); @@ -370,6 +511,13 @@ class TicketViewer extends React.Component { ); } + searchTickets(filters) { + const SEARCH_TICKETS_PATH = '/admin/panel/tickets/search-tickets'; + const urlQuery = filters && searchTicketsUtils.getFiltersForURL({filters}); + + return urlQuery && `${SEARCH_TICKETS_PATH}${urlQuery}`; + } + getCommentFormProps() { return { onSubmit: this.onSubmit.bind(this), @@ -424,10 +572,18 @@ class TicketViewer extends React.Component { } onReopenClick() { + this.setState({ + editStatus: false + }); + AreYouSure.openModal(null, this.reopenTicket.bind(this)); } onCloseTicketClick(event) { + this.setState({ + editStatus: false + }); + event.preventDefault(); AreYouSure.openModal(null, this.closeTicket.bind(this)); } @@ -646,47 +802,77 @@ class TicketViewer extends React.Component { } onTicketModification() { - if (this.props.onChange) { - this.props.onChange(); + const { onChange } = this.props; + if(onChange) { + onChange(); } } getStaffAssignmentItems() { - const {staffMembers, userDepartments, userId, ticket} = this.props; + const { + userDepartments, + userId, + ticket + } = this.props; const ticketDepartmentId = ticket.department.id; + const FIRST_ITEM = 0; let staffAssignmentItems = [ - {content: 'None', id: 0} + {content: i18n('NONE'), contentOnSelected: i18n('NONE'), id: 0} ]; if(_.some(userDepartments, {id: ticketDepartmentId})) { - staffAssignmentItems.push({content: i18n('ASSIGN_TO_ME'), id: userId}); + staffAssignmentItems.push({ + content: i18n('ASSIGN_TO_ME'), + contentOnSelected: this.getStaffList({onlyMe: true})[FIRST_ITEM].name, + id: userId + }); } staffAssignmentItems = staffAssignmentItems.concat( _.map( - _.filter(staffMembers, ({id, departments}) => { - return (id != userId) && _.some(departments, {id: ticketDepartmentId}); - }), - ({id, name}) => ({content: name, id: id*1}) + this.getStaffList({onlyMe: false}), + ({id, name}) => ({content: name, contentOnSelected: name, id: id*1}) ) ); return staffAssignmentItems; } + getStaffList(onlyMeObject) { + const { + userId, + staffMembers, + ticket + } = this.props; + + return _.filter(staffMembers, ({id, departments}) => { + const idComparer = onlyMeObject.onlyMe ? (id == userId) : (id != userId); + + return idComparer && _.some(departments, {id: ticket.department.id}); + }) + } + getDepartmentsForTransfer() { return this.props.ticket.author.staff ? SessionStore.getDepartments() : this.getPublicDepartments(); } showDeleteButton() { - if(!this.props.ticket.owner) { - if(this.props.userLevel === 3) return true; - if(this.props.userId == this.props.ticket.author.id*1) { - if((this.props.userStaff && this.props.ticket.author.staff) || (!this.props.userStaff && !this.props.ticket.author.staff)){ + const { + ticket, + userLevel, + userId, + userStaff + } = this.props; + + if(!ticket.owner) { + if(userLevel === 3) return true; + if(userId == ticket.author.id*1) { + if((userStaff && ticket.author.staff) || (!userStaff && !ticket.author.staff)){ return true; } } } + return false; } } diff --git a/client/src/app-components/ticket-viewer.scss b/client/src/app-components/ticket-viewer.scss index 79c3cd8e..55d695fa 100644 --- a/client/src/app-components/ticket-viewer.scss +++ b/client/src/app-components/ticket-viewer.scss @@ -26,15 +26,34 @@ margin: 0 10px; } - &__edit-title-icon { - color: #414A59; + &__edit-icon { right: 12px; margin: 0 10px; + color: $light-grey; + &:hover { cursor:pointer; } } + &__edit-title-icon { + color: $primary-blue; + right: 12px; + margin: 0 10px; + + &:hover { + cursor:pointer; + } + } + + &__edit-status__buttons { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-top: 10px; + } + &___input-edit-title { color: black; align-items:center; @@ -87,10 +106,24 @@ &-container-editable { display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; + flex-direction: row; + justify-content: center; + align-items: flex-start; min-width: 300px; + + &:hover { + .ticket-viewer__edit-icon { + color: $primary-blue; + } + } + + &__column { + display: flex; + flex-direction: column; + justify-content: space-around; + align-items: center; + min-width: 170px; + } } &-header { @@ -100,7 +133,7 @@ &-value { color: $secondary-blue; padding-bottom: 10px; - + width: 100%; } } diff --git a/client/src/core-components/autocomplete.scss b/client/src/core-components/autocomplete.scss index 45cc37ee..b567b074 100644 --- a/client/src/core-components/autocomplete.scss +++ b/client/src/core-components/autocomplete.scss @@ -7,14 +7,14 @@ &__drop-down { .drop-down__current-item { cursor: text; - background-color: white; + background-color: $very-light-grey; border: 1px solid $grey; min-height: 38px; &:focus-within { outline: none; border-color: $primary-blue; - background-color: white; + background-color: $very-light-grey; } } diff --git a/client/src/core-components/drop-down.js b/client/src/core-components/drop-down.js index acc76beb..fc0f2a52 100644 --- a/client/src/core-components/drop-down.js +++ b/client/src/core-components/drop-down.js @@ -89,7 +89,7 @@ class DropDown extends React.Component { } renderCurrentItem() { - let item = this.props.items[this.getSelectedIndex()]; + const item = this.props.items[this.getSelectedIndex()]; let iconNode = null; if (item.icon) { @@ -98,7 +98,7 @@ class DropDown extends React.Component { return (
- {iconNode}{item.content} + {iconNode}{item.contentOnSelected ? item.contentOnSelected : item.content}
); }