From 27d990d0b51b5658d961cfa9b37507fb0ec3f60f Mon Sep 17 00:00:00 2001 From: Rafael Ferreira Date: Mon, 21 Dec 2020 23:53:35 -0300 Subject: [PATCH 1/3] [FEATURE] Omnichannel / Directory - Contacts Tab (#19904) * [FEATURE] Directory page starting contacts/visitors tab. * [IMPROVE] Removing unused variable. * [IMPROVE] Directory link tooltip to just "Directory" instead "Omnichannel Directory" * Implementing improves in contact tab, routes, last chat column mock and new button aside filter * Contextual bar and routes for info and new contacts * Contact endpoints for omnichannel directory forms. * "Last Chat" missing translation * Omnichannel Directory - Create new contact form. * view a specific contact and improves at contact form. * "edit" contact form with same "create" component * Show custom fields in Contact Info page. * handling custom fields on edit/create contacts. * Showing only "custom fields" that are visible and with "visitor" scope. * CreatedAt data in Contact/Visitor and ContactInfo. * Add LastChat data in Contact/Visitor. * Show LastChat in Contact Info page and Grid * Email validation improvement and asterisk on required field * Starting "contact manager" on edit/create page. * show contact manager user info * closing contextbar when you click at cancel button, removing chat history button and fixing contact manager field. * Change useEndpointDataExperimental to useEndpointData. * Custom fields required with asterisk. * just using the LivechatVisitors.findOneById method * changing POST method to use existing method to save visitors/contacts, renaming contactManager data. * PUT not accepted so we'll use POST to save and update contacts. * Set last chat when create a new room. * Changing place of updateLastChat method. * Using correct translation aliases. * Contact Manager Info to EE folder and renaming ContactForm. * Moving EE contact manager input to proper path. * verify if user has authorization to see contacts page. * Fixing blank return useless. * using callback to lastChat * Add small improvements to the code. Co-authored-by: Renato Becker --- app/livechat/server/api/lib/visitors.js | 1 + app/livechat/server/api/rest.js | 1 + app/livechat/server/api/v1/contact.js | 44 ++++ app/livechat/server/api/v1/room.js | 2 + .../server/hooks/saveContactLastChat.js | 12 + app/livechat/server/index.js | 1 + app/livechat/server/lib/Livechat.js | 37 ++- client/components/CustomFieldsForm.js | 10 +- client/components/FilterByText.tsx | 11 +- client/components/UserCard.js | 2 +- client/contexts/OmnichannelContext.ts | 1 + client/omnichannel/directory/ContactForm.js | 213 ++++++++++++++++++ client/omnichannel/directory/ContactInfo.js | 112 +++++++++ client/omnichannel/directory/ContactTab.js | 97 ++++++++ .../directory/OmnichannelDirectoryPage.js | 74 ++++++ client/omnichannel/directory/Skeleton.js | 11 + client/routes.js | 10 + client/sidebar/sections/Omnichannel.js | 9 +- ee/client/omnichannel/ContactManager.js | 27 +++ .../additionalForms/ContactManager.js | 18 ++ .../omnichannel/additionalForms/register.js | 1 + packages/rocketchat-i18n/i18n/en.i18n.json | 24 ++ packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 3 + 23 files changed, 706 insertions(+), 15 deletions(-) create mode 100644 app/livechat/server/api/v1/contact.js create mode 100644 app/livechat/server/hooks/saveContactLastChat.js create mode 100644 client/omnichannel/directory/ContactForm.js create mode 100644 client/omnichannel/directory/ContactInfo.js create mode 100644 client/omnichannel/directory/ContactTab.js create mode 100644 client/omnichannel/directory/OmnichannelDirectoryPage.js create mode 100644 client/omnichannel/directory/Skeleton.js create mode 100644 ee/client/omnichannel/ContactManager.js create mode 100644 ee/client/omnichannel/additionalForms/ContactManager.js diff --git a/app/livechat/server/api/lib/visitors.js b/app/livechat/server/api/lib/visitors.js index d8cf2763571d..c0366bf1ac69 100644 --- a/app/livechat/server/api/lib/visitors.js +++ b/app/livechat/server/api/lib/visitors.js @@ -144,6 +144,7 @@ export async function findVisitorsByEmailOrPhoneOrNameOrUsername({ userId, term, phone: 1, livechatData: 1, visitorEmails: 1, + lastChat: 1, }, }); diff --git a/app/livechat/server/api/rest.js b/app/livechat/server/api/rest.js index a63794bf1db0..1991a9f2fce9 100644 --- a/app/livechat/server/api/rest.js +++ b/app/livechat/server/api/rest.js @@ -9,3 +9,4 @@ import './v1/customField.js'; import './v1/room.js'; import './v1/videoCall.js'; import './v1/transfer.js'; +import './v1/contact.js'; diff --git a/app/livechat/server/api/v1/contact.js b/app/livechat/server/api/v1/contact.js new file mode 100644 index 000000000000..4099fb49910a --- /dev/null +++ b/app/livechat/server/api/v1/contact.js @@ -0,0 +1,44 @@ +import { Match, check } from 'meteor/check'; + +import { API } from '../../../../api/server'; +import { Livechat } from '../../lib/Livechat'; +import { + LivechatVisitors, +} from '../../../../models'; + +const createToken = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + +API.v1.addRoute('omnichannel/contact', { authRequired: true }, { + post() { + try { + check(this.bodyParams, { + _id: Match.Maybe(String), + name: String, + email: Match.Maybe(String), + phone: Match.Maybe(String), + livechatData: Match.Maybe(Object), + contactManager: Match.Maybe(Object), + }); + + const contactParams = this.bodyParams; + if (this.bodyParams.phone) { + contactParams.phone = { number: this.bodyParams.phone }; + } + + const data = Object.assign(contactParams, { token: createToken() }); + const contact = Livechat.registerGuest(data); + return API.v1.success({ contact }); + } catch (e) { + return API.v1.failure(e); + } + }, + get() { + check(this.queryParams, { + contactId: String, + }); + + const contact = Promise.await(LivechatVisitors.findOneById(this.queryParams.contactId)); + + return API.v1.success({ contact }); + }, +}); diff --git a/app/livechat/server/api/v1/room.js b/app/livechat/server/api/v1/room.js index 3526bf2cd623..8ce9d42014af 100644 --- a/app/livechat/server/api/v1/room.js +++ b/app/livechat/server/api/v1/room.js @@ -11,6 +11,7 @@ import { Livechat } from '../../lib/Livechat'; import { normalizeTransferredByData } from '../../lib/Helper'; import { findVisitorInfo } from '../lib/visitors'; + API.v1.addRoute('livechat/room', { get() { const defaultCheckParams = { @@ -40,6 +41,7 @@ API.v1.addRoute('livechat/room', { const rid = roomId || Random.id(); const room = Promise.await(getRoom({ guest, rid, agent, extraParams })); + return API.v1.success(room); } catch (e) { return API.v1.failure(e); diff --git a/app/livechat/server/hooks/saveContactLastChat.js b/app/livechat/server/hooks/saveContactLastChat.js new file mode 100644 index 000000000000..91d560d65879 --- /dev/null +++ b/app/livechat/server/hooks/saveContactLastChat.js @@ -0,0 +1,12 @@ +import { callbacks } from '../../../callbacks'; +import { Livechat } from '../lib/Livechat'; + +callbacks.add('livechat.newRoom', (room) => { + const { _id, v: { _id: guestId } } = room; + + const lastChat = { + _id, + ts: new Date(), + }; + Livechat.updateLastChat(guestId, lastChat); +}, callbacks.priority.MEDIUM, 'livechat-save-last-chat'); diff --git a/app/livechat/server/index.js b/app/livechat/server/index.js index ad8f2ca1c2d7..435627266cf4 100644 --- a/app/livechat/server/index.js +++ b/app/livechat/server/index.js @@ -19,6 +19,7 @@ import './hooks/processRoomAbandonment'; import './hooks/saveLastVisitorMessageTs'; import './hooks/markRoomNotResponded'; import './hooks/sendTranscriptOnClose'; +import './hooks/saveContactLastChat'; import './methods/addAgent'; import './methods/addManager'; import './methods/changeLivechatStatus'; diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index d5fd07399ebf..dbd53debd3c0 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -149,7 +149,6 @@ export const Livechat = { if (guest.name) { message.alias = guest.name; } - // return messages; return _.extend(sendMessage(guest, message, room), { newRoom, showConnecting: this.showConnecting() }); }, @@ -193,7 +192,7 @@ export const Livechat = { return true; }, - registerGuest({ token, name, email, department, phone, username, connectionData } = {}) { + registerGuest({ token, name, email, department, phone, username, livechatData, contactManager, connectionData } = {}) { check(token, String); let userId; @@ -201,6 +200,7 @@ export const Livechat = { $set: { token, }, + $unset: { }, }; const user = LivechatVisitors.getVisitorByToken(token, { fields: { _id: 1 } }); @@ -219,6 +219,7 @@ export const Livechat = { } else { const userData = { username, + ts: new Date(), }; if (settings.get('Livechat_Allow_collect_and_store_HTTP_header_informations')) { @@ -234,29 +235,45 @@ export const Livechat = { } } + if (name) { + updateUser.$set.name = name; + } + if (phone) { updateUser.$set.phone = [ { phoneNumber: phone.number }, ]; + } else { + updateUser.$unset.phone = 1; } if (email && email.trim() !== '') { updateUser.$set.visitorEmails = [ { address: email }, ]; + } else { + updateUser.$unset.visitorEmails = 1; } - if (name) { - updateUser.$set.name = name; + if (livechatData) { + updateUser.$set.livechatData = livechatData; + } else { + updateUser.$unset.livechatData = 1; + } + + if (contactManager) { + updateUser.$set.contactManager = contactManager; + } else { + updateUser.$unset.contactManager = 1; } if (!department) { - Object.assign(updateUser, { $unset: { department: 1 } }); + updateUser.$unset.department = 1; } else { const dep = LivechatDepartment.findOneByIdOrName(department); updateUser.$set.department = dep && dep._id; } - + if (_.isEmpty(updateUser.$unset)) { delete updateUser.$unset; } LivechatVisitors.updateById(userId, updateUser); return userId; @@ -1155,6 +1172,14 @@ export const Livechat = { return LivechatRooms.findOneById(roomId); }, + updateLastChat(contactId, lastChat) { + const updateUser = { + $set: { + lastChat, + }, + }; + LivechatVisitors.updateById(contactId, updateUser); + }, }; settings.get('Livechat_history_monitor_type', (key, value) => { diff --git a/client/components/CustomFieldsForm.js b/client/components/CustomFieldsForm.js index dd5958929007..9af38197c558 100644 --- a/client/components/CustomFieldsForm.js +++ b/client/components/CustomFieldsForm.js @@ -24,7 +24,7 @@ const CustomTextInput = ({ name, required, minLength, maxLength, setState, state }, [state, required, minLength, t]); return useMemo(() => - {t(name)} + {t(name)}{required && '*'} setState(e.currentTarget.value)}/> @@ -38,7 +38,7 @@ const CustomSelect = ({ name, required, options = {}, setState, state, className const verify = useMemo(() => (!state.length && required ? t('Field_required') : ''), [required, state.length, t]); return useMemo(() => - {t(name)} + {t(name)}{required && '*'}