diff --git a/.changeset/early-oranges-doubt.md b/.changeset/early-oranges-doubt.md new file mode 100644 index 000000000000..22effd8537fc --- /dev/null +++ b/.changeset/early-oranges-doubt.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Allows agents to add multiple emails and phone numbers to a contact diff --git a/apps/meteor/client/omnichannel/additionalForms/ContactManager.js b/apps/meteor/client/omnichannel/additionalForms/ContactManagerInput.tsx similarity index 73% rename from apps/meteor/client/omnichannel/additionalForms/ContactManager.js rename to apps/meteor/client/omnichannel/additionalForms/ContactManagerInput.tsx index 52ab527ef841..1773ab8b5e36 100644 --- a/apps/meteor/client/omnichannel/additionalForms/ContactManager.js +++ b/apps/meteor/client/omnichannel/additionalForms/ContactManagerInput.tsx @@ -5,7 +5,12 @@ import React from 'react'; import AutoCompleteAgent from '../../components/AutoCompleteAgent'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; -export const ContactManager = ({ value: userId, handler }) => { +type ContactManagerInputProps = { + value: string; + handler: (currentValue: string) => void; +}; + +export const ContactManagerInput = ({ value: userId, handler }: ContactManagerInputProps) => { const t = useTranslation(); const hasLicense = useHasLicenseModule('livechat-enterprise'); @@ -23,4 +28,4 @@ export const ContactManager = ({ value: userId, handler }) => { ); }; -export default ContactManager; +export default ContactManagerInput; diff --git a/apps/meteor/client/views/omnichannel/additionalForms.tsx b/apps/meteor/client/views/omnichannel/additionalForms.tsx index 824b5eb69694..ef2c41757244 100644 --- a/apps/meteor/client/views/omnichannel/additionalForms.tsx +++ b/apps/meteor/client/views/omnichannel/additionalForms.tsx @@ -1,5 +1,5 @@ import BusinessHoursMultiple from '../../omnichannel/additionalForms/BusinessHoursMultiple'; -import ContactManager from '../../omnichannel/additionalForms/ContactManager'; +import ContactManagerInput from '../../omnichannel/additionalForms/ContactManagerInput'; import CurrentChatTags from '../../omnichannel/additionalForms/CurrentChatTags'; import CustomFieldsAdditionalForm from '../../omnichannel/additionalForms/CustomFieldsAdditionalForm'; import DepartmentBusinessHours from '../../omnichannel/additionalForms/DepartmentBusinessHours'; @@ -20,7 +20,7 @@ export { EeTextAreaInput, BusinessHoursMultiple, EeTextInput, - ContactManager, + ContactManagerInput, CurrentChatTags, DepartmentBusinessHours, DepartmentForwarding, diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfo.tsx similarity index 66% rename from apps/meteor/client/views/omnichannel/contactInfo/ContactInfo.tsx rename to apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfo.tsx index 22aa14350a99..8bfc37587db9 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfo.tsx @@ -1,26 +1,23 @@ +import type { ILivechatContact, Serialized } from '@rocket.chat/core-typings'; import { Box, IconButton, Tabs, TabsItem } from '@rocket.chat/fuselage'; import { UserAvatar } from '@rocket.chat/ui-avatar'; -import type { RouteName } from '@rocket.chat/ui-contexts'; import { useTranslation, useEndpoint, usePermission, useRouter, useRouteParameter } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../components/Contextualbar'; -import { useFormatDate } from '../../../hooks/useFormatDate'; -import { FormSkeleton } from '../directory/components/FormSkeleton'; -import { useContactRoute } from '../hooks/useContactRoute'; -import ContactInfoChannels from './tabs/ContactInfoChannels'; -import ContactInfoDetails from './tabs/ContactInfoDetails'; -import ContactInfoHistory from './tabs/ContactInfoHistory'; +import { ContextualbarHeader, ContextualbarIcon, ContextualbarTitle, ContextualbarClose } from '../../../../components/Contextualbar'; +import { useFormatDate } from '../../../../hooks/useFormatDate'; +import { useContactRoute } from '../../hooks/useContactRoute'; +import ContactInfoChannels from '../tabs/ContactInfoChannels'; +import ContactInfoDetails from '../tabs/ContactInfoDetails'; +import ContactInfoHistory from '../tabs/ContactInfoHistory'; type ContactInfoProps = { - id: string; + contact: Serialized; onClose: () => void; - rid?: string; - route?: RouteName; }; -const ContactInfo = ({ id: contactId, onClose }: ContactInfoProps) => { +const ContactInfo = ({ contact, onClose }: ContactInfoProps) => { const t = useTranslation(); const { getRouteName } = useRouter(); @@ -36,34 +33,10 @@ const ContactInfo = ({ id: contactId, onClose }: ContactInfoProps) => { const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields'); const { data: { customFields } = {} } = useQuery(['/v1/livechat/custom-fields'], () => getCustomFields()); - const getContact = useEndpoint('GET', '/v1/omnichannel/contact'); - const { - data: { contact } = {}, - isInitialLoading, - isError, - } = useQuery(['/v1/omnichannel/contact', contactId], () => getContact({ contactId }), { - enabled: canViewCustomFields && !!contactId, - }); - - if (isInitialLoading) { - return ( - - - - ); - } - - if (isError || !contact) { - return {t('Contact_not_found')}; - } - - const { username, visitorEmails, phone, ts, livechatData, lastChat, contactManager } = contact; + const { name, emails, phones, createdAt, lastChat, contactManager, customFields: userCustomFields } = contact; const showContactHistory = (currentRouteName === 'live' || currentRouteName === 'omnichannel-directory') && lastChat; - const [{ phoneNumber = '' }] = phone ?? [{}]; - const [{ address: email = '' }] = visitorEmails ?? [{}]; - const checkIsVisibleAndScopeVisitor = (key: string) => { const field = customFields?.find(({ _id }) => _id === key); return field?.visibility === 'visible' && field?.scope === 'visitor'; @@ -71,7 +44,7 @@ const ContactInfo = ({ id: contactId, onClose }: ContactInfoProps) => { // Serialized does not like unknown :( const customFieldEntries = canViewCustomFields - ? Object.entries((livechatData ?? {}) as unknown as Record).filter( + ? Object.entries((userCustomFields ?? {}) as unknown as Record).filter( ([key, value]) => checkIsVisibleAndScopeVisitor(key) && value, ) : []; @@ -84,12 +57,12 @@ const ContactInfo = ({ id: contactId, onClose }: ContactInfoProps) => { - {username && ( + {name && ( - + - {username} + {name} {lastChat && {`${t('Last_Chat')}: ${formatDate(lastChat.ts)}`}} @@ -118,10 +91,10 @@ const ContactInfo = ({ id: contactId, onClose }: ContactInfoProps) => { {context === 'details' && ( phoneNumber)} + emails={emails?.map(({ address }) => address)} customFieldEntries={customFieldEntries} /> )} diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfoWithData.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfoWithData.tsx new file mode 100644 index 000000000000..6da95fb99c46 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/ContactInfoWithData.tsx @@ -0,0 +1,34 @@ +import { Box } from '@rocket.chat/fuselage'; +import { useEndpoint, usePermission, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import React from 'react'; + +import { ContextualbarSkeleton } from '../../../../components/Contextualbar'; +import ContactInfo from './ContactInfo'; + +type ContactInfoWithDataProps = { + id: string; + onClose: () => void; +}; + +const ContactInfoWithData = ({ id: contactId, onClose }: ContactInfoWithDataProps) => { + const t = useTranslation(); + const canViewCustomFields = usePermission('view-livechat-room-customfields'); + + const getContact = useEndpoint('GET', '/v1/omnichannel/contacts.get'); + const { data, isInitialLoading, isError } = useQuery(['getContactById', contactId], () => getContact({ contactId }), { + enabled: canViewCustomFields && !!contactId, + }); + + if (isInitialLoading) { + return ; + } + + if (isError || !data?.contact) { + return {t('Contact_not_found')}; + } + + return ; +}; + +export default ContactInfoWithData; diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/index.ts b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/index.ts new file mode 100644 index 000000000000..59e2beece146 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfo/index.ts @@ -0,0 +1 @@ +export { default } from './ContactInfoWithData'; diff --git a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfoRouter.tsx b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfoRouter.tsx index 23201497fb79..30a35f8610fc 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/ContactInfoRouter.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/ContactInfoRouter.tsx @@ -18,14 +18,14 @@ const ContactInfoRouter = () => { }; const { - v: { _id }, + v: { contactId }, } = room; if (context === 'edit') { - return ; + return ; } - return ; + return ; }; export default ContactInfoRouter; diff --git a/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfo.tsx b/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfo.tsx index 8cb79d712c69..ddfcfa4973e9 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfo.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfo.tsx @@ -1,11 +1,12 @@ -import type { ILivechatVisitor, Serialized } from '@rocket.chat/core-typings'; -import { Field, FieldLabel, FieldRow, FieldError, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage'; +import type { ILivechatContact, Serialized } from '@rocket.chat/core-typings'; +import { Field, FieldLabel, FieldRow, FieldError, TextInput, ButtonGroup, Button, IconButton, Divider } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { CustomFieldsForm } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; -import React, { useState, useEffect } from 'react'; -import { useForm } from 'react-hook-form'; +import React, { Fragment } from 'react'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { hasAtLeastOnePermission } from '../../../../app/authorization/client'; import { validateEmail } from '../../../../lib/emailValidator'; @@ -18,34 +19,30 @@ import { ContextualbarTitle, ContextualbarClose, } from '../../../components/Contextualbar'; -import { createToken } from '../../../lib/utils/createToken'; -import { ContactManager as ContactManagerForm } from '../additionalForms'; +import { ContactManagerInput } from '../additionalForms'; import { FormSkeleton } from '../directory/components/FormSkeleton'; import { useCustomFieldsMetadata } from '../directory/hooks/useCustomFieldsMetadata'; import { useContactRoute } from '../hooks/useContactRoute'; type ContactNewEditProps = { - id: string; - contactData?: { contact: Serialized | null }; + contactData?: Serialized | null; onClose: () => void; onCancel: () => void; }; type ContactFormData = { - token: string; name: string; - email: string; - phone: string; - username: string; + emails: { address: string }[]; + phones: { phoneNumber: string }[]; customFields: Record; + contactManager: string; }; const DEFAULT_VALUES = { - token: '', name: '', - email: '', - phone: '', - username: '', + emails: [], + phones: [], + contactManager: '', customFields: {}, }; @@ -54,136 +51,118 @@ const getInitialValues = (data: ContactNewEditProps['contactData']): ContactForm return DEFAULT_VALUES; } - const { name, token, phone, visitorEmails, livechatData, contactManager } = data.contact ?? {}; + const { name, phones, emails, customFields, contactManager } = data ?? {}; return { - token: token ?? '', name: name ?? '', - email: visitorEmails ? visitorEmails[0].address : '', - phone: phone ? phone[0].phoneNumber : '', - customFields: livechatData ?? {}, - username: contactManager?.username ?? '', + emails: emails ?? [], + phones: phones ?? [], + customFields: customFields ?? {}, + contactManager: contactManager ?? '', }; }; -const EditContactInfo = ({ id, contactData, onClose, onCancel }: ContactNewEditProps): ReactElement => { +const EditContactInfo = ({ contactData, onClose, onCancel }: ContactNewEditProps): ReactElement => { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const queryClient = useQueryClient(); const handleNavigate = useContactRoute(); - const canViewCustomFields = (): boolean => - hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']); + const canViewCustomFields = hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']); - const [userId, setUserId] = useState('no-agent-selected'); - const saveContact = useEndpoint('POST', '/v1/omnichannel/contact'); - const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search'); - const getUserData = useEndpoint('GET', '/v1/users.info'); + const getContact = useEndpoint('GET', '/v1/omnichannel/contacts.get'); + const createContact = useEndpoint('POST', '/v1/omnichannel/contacts'); + const updateContact = useEndpoint('POST', '/v1/omnichannel/contacts.update'); const { data: customFieldsMetadata = [], isInitialLoading: isLoadingCustomFields } = useCustomFieldsMetadata({ scope: 'visitor', - enabled: canViewCustomFields(), + enabled: canViewCustomFields, }); const initialValue = getInitialValues(contactData); - const { username: initialUsername } = initialValue; const { - register, - formState: { errors, isValid, isDirty, isSubmitting }, + formState: { errors, isSubmitting }, control, - setValue, + watch, handleSubmit, - setError, } = useForm({ - mode: 'onChange', - reValidateMode: 'onChange', + mode: 'onBlur', + reValidateMode: 'onBlur', defaultValues: initialValue, }); - useEffect(() => { - if (!initialUsername) { - return; - } + const { + fields: emailFields, + append: appendEmail, + remove: removeEmail, + } = useFieldArray({ + control, + name: 'emails', + }); - getUserData({ username: initialUsername }).then(({ user }) => { - setUserId(user._id); - }); - }, [getUserData, initialUsername]); + const { + fields: phoneFields, + append: appendPhone, + remove: removePhone, + } = useFieldArray({ + control, + name: 'phones', + }); - const validateEmailFormat = (email: string): boolean | string => { - if (!email || email === initialValue.email) { - return true; - } + const { emails, phones } = watch(); - if (!validateEmail(email)) { + const validateEmailFormat = async (emailValue: string) => { + const currentEmails = emails.map(({ address }) => address); + const isDuplicated = currentEmails.filter((email) => email === emailValue).length > 1; + + if (!validateEmail(emailValue)) { return t('error-invalid-email-address'); } - return true; + const { contact } = await getContact({ email: emailValue }); + return (!contact || contact._id === contactData?._id) && !isDuplicated ? true : t('Email_already_exists'); }; - const validateContactField = async (name: 'phone' | 'email', value: string, optional = true) => { - if ((optional && !value) || value === initialValue[name]) { - return true; - } + const validatePhone = async (phoneValue: string) => { + const currentPhones = phones.map(({ phoneNumber }) => phoneNumber); + const isDuplicated = currentPhones.filter((phone) => phone === phoneValue).length > 1; - const query = { [name]: value } as Record<'phone' | 'email', string>; - const { contact } = await getContactBy(query); - return !contact || contact._id === id; + const { contact } = await getContact({ phone: phoneValue }); + return (!contact || contact._id === contactData?._id) && !isDuplicated ? true : t('Phone_already_exists'); }; const validateName = (v: string): string | boolean => (!v.trim() ? t('Required_field', { field: t('Name') }) : true); - const handleContactManagerChange = async (userId: string): Promise => { - setUserId(userId); - - if (userId === 'no-agent-selected') { - setValue('username', '', { shouldDirty: true }); - return; - } - - const { user } = await getUserData({ userId }); - setValue('username', user.username || '', { shouldDirty: true }); - }; - - const validateAsync = async ({ phone = '', email = '' } = {}) => { - const isEmailValid = await validateContactField('email', email); - const isPhoneValid = await validateContactField('phone', phone); - - !isEmailValid && setError('email', { message: t('Email_already_exists') }); - !isPhoneValid && setError('phone', { message: t('Phone_already_exists') }); - - return isEmailValid && isPhoneValid; - }; - const handleSave = async (data: ContactFormData): Promise => { - if (!(await validateAsync(data))) { - return; - } - - const { name, phone, email, customFields, username, token } = data; + const { name, phones, emails, customFields, contactManager } = data; const payload = { name, - phone, - email, + phones: phones.map(({ phoneNumber }) => phoneNumber), + emails: emails.map(({ address }) => address), customFields, - token: token || createToken(), - ...(username && { contactManager: { username } }), - ...(id && { _id: id }), + contactManager, }; try { - await saveContact(payload); + if (contactData) { + await updateContact({ contactId: contactData?._id, ...payload }); + handleNavigate({ context: 'details', id: contactData?._id }); + } else { + const { contactId } = await createContact(payload); + handleNavigate({ context: 'details', id: contactId }); + } + dispatchToastMessage({ type: 'success', message: t('Saved') }); await queryClient.invalidateQueries({ queryKey: ['current-contacts'] }); - contactData ? handleNavigate({ context: 'details' }) : handleNavigate({ tab: 'contacts', context: '' }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }; + const nameField = useUniqueId(); + if (isLoadingCustomFields) { return ( @@ -201,43 +180,89 @@ const EditContactInfo = ({ id, contactData, onClose, onCancel }: ContactNewEditP - {t('Name')}* + + {t('Name')} + - + } + /> - {errors.name?.message} + {errors.name && {errors.name.message}} {t('Email')} - - - - {errors.email?.message} + {emailFields.map((field, index) => ( + + + } + /> + removeEmail(index)} mis={8} icon='trash' /> + + {errors.emails?.[index]?.address && {errors.emails?.[index]?.address?.message}} + + ))} + {t('Phone')} - - - - {errors.phone?.message} + {phoneFields.map((field, index) => ( + + + } + /> + removePhone(index)} mis={8} icon='trash' /> + + {errors.phones?.[index]?.phoneNumber && {errors.phones?.[index]?.phoneNumber?.message}} + {errors.phones?.[index]?.message} + + ))} + - {canViewCustomFields() && } - + ( + { + if (currentValue === 'no-agent-selected') { + return onChange(''); + } + + onChange(currentValue); + }} + /> + )} + /> + + {canViewCustomFields && } - - + diff --git a/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfoWithData.tsx b/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfoWithData.tsx index 8fac0a5baccc..68137b02e75e 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfoWithData.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/EditContactInfoWithData.tsx @@ -6,10 +6,16 @@ import React from 'react'; import { FormSkeleton } from '../directory/components/FormSkeleton'; import EditContactInfo from './EditContactInfo'; -const EditContactInfoWithData = ({ id, onClose, onCancel }: { id: string; onClose: () => void; onCancel: () => void }) => { +type EditContactInfoWithDataProps = { + id: string; + onClose: () => void; + onCancel: () => void; +}; + +const EditContactInfoWithData = ({ id, onClose, onCancel }: EditContactInfoWithDataProps) => { const t = useTranslation(); - const getContactEndpoint = useEndpoint('GET', '/v1/omnichannel/contact'); + const getContactEndpoint = useEndpoint('GET', '/v1/omnichannel/contacts.get'); const { data, isLoading, isError } = useQuery(['getContactById', id], async () => getContactEndpoint({ contactId: id })); if (isLoading) { @@ -24,7 +30,7 @@ const EditContactInfoWithData = ({ id, onClose, onCancel }: { id: string; onClos return {t('Contact_not_found')}; } - return ; + return ; }; export default EditContactInfoWithData; diff --git a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetails.tsx b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetails.tsx index fbb304d73aeb..9ece7d02f330 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetails.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetails.tsx @@ -4,38 +4,35 @@ import React from 'react'; import { ContextualbarScrollableContent } from '../../../../../components/Contextualbar'; import { useFormatDate } from '../../../../../hooks/useFormatDate'; -import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber'; import CustomField from '../../../components/CustomField'; import Field from '../../../components/Field'; import Info from '../../../components/Info'; import Label from '../../../components/Label'; -import ContactInfoDetailsEntry from './ContactInfoDetailsEntry'; +import ContactInfoDetailsGroup from './ContactInfoDetailsGroup'; import ContactManagerInfo from './ContactManagerInfo'; type ContactInfoDetailsProps = { - email: string; - phoneNumber: string; - ts: string; + emails?: string[]; + phones?: string[]; + createdAt: string; customFieldEntries: [string, string][]; - contactManager?: { - username: string; - }; + contactManager?: string; }; -const ContactInfoDetails = ({ email, phoneNumber, ts, customFieldEntries, contactManager }: ContactInfoDetailsProps) => { +const ContactInfoDetails = ({ emails, phones, createdAt, customFieldEntries, contactManager }: ContactInfoDetailsProps) => { const t = useTranslation(); const formatDate = useFormatDate(); return ( - {email && } - {phoneNumber && } - {contactManager && } + {emails?.length ? : null} + {phones?.length ? : null} + {contactManager && } - {ts && ( + {createdAt && ( - {formatDate(ts)} + {formatDate(createdAt)} )} {customFieldEntries.length > 0 && } diff --git a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsEntry.tsx b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsEntry.tsx index 81764d9e6000..c6fcc9bf9546 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsEntry.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsEntry.tsx @@ -4,6 +4,7 @@ import React from 'react'; import { useIsCallReady } from '../../../../../contexts/CallContext'; import useClipboardWithToast from '../../../../../hooks/useClipboardWithToast'; +import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber'; import ContactInfoCallButton from './ContactInfoCallButton'; type ContactInfoDetailsEntryProps = { @@ -12,27 +13,22 @@ type ContactInfoDetailsEntryProps = { value: string; }; -const ContactInfoDetailsEntry = ({ type, label, value }: ContactInfoDetailsEntryProps) => { +const ContactInfoDetailsEntry = ({ type, value }: ContactInfoDetailsEntryProps) => { const t = useTranslation(); const { copy } = useClipboardWithToast(value); const isCallReady = useIsCallReady(); return ( - - - {label} - - - - - - {value} - - - {isCallReady && type === 'phone' && } - copy()} tiny title={t('Copy')} icon='copy' /> - + + + + + {type === 'phone' ? parseOutboundPhoneNumber(value) : value} + + + {isCallReady && type === 'phone' && } + copy()} tiny title={t('Copy')} icon='copy' /> diff --git a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx new file mode 100644 index 000000000000..82201409f7a8 --- /dev/null +++ b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactInfoDetailsGroup.tsx @@ -0,0 +1,25 @@ +import { Box } from '@rocket.chat/fuselage'; +import React from 'react'; + +import ContactInfoDetailsEntry from './ContactInfoDetailsEntry'; + +type ContactInfoDetailsGroupProps = { + type: 'phone' | 'email'; + label: string; + values: string[]; +}; + +const ContactInfoDetailsGroup = ({ type, label, values }: ContactInfoDetailsGroupProps) => { + return ( + + + {label} + + {values.map((value, index) => ( + + ))} + + ); +}; + +export default ContactInfoDetailsGroup; diff --git a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactManagerInfo.tsx b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactManagerInfo.tsx index ff74f5add91c..f8a58fec5579 100644 --- a/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactManagerInfo.tsx +++ b/apps/meteor/client/views/omnichannel/contactInfo/tabs/ContactInfoDetails/ContactManagerInfo.tsx @@ -6,13 +6,13 @@ import React from 'react'; import { UserStatus } from '../../../../../components/UserStatus'; -type ContactManagerInfoProps = { username: string }; +type ContactManagerInfoProps = { userId: string }; -const ContactManagerInfo = ({ username }: ContactManagerInfoProps) => { +const ContactManagerInfo = ({ userId }: ContactManagerInfoProps) => { const t = useTranslation(); const getContactManagerByUsername = useEndpoint('GET', '/v1/users.info'); - const { data, isLoading } = useQuery(['getContactManagerByUsername', username], async () => getContactManagerByUsername({ username })); + const { data, isLoading } = useQuery(['getContactManagerByUserId', userId], async () => getContactManagerByUsername({ userId })); if (isLoading) { return null; @@ -22,7 +22,7 @@ const ContactManagerInfo = ({ username }: ContactManagerInfoProps) => { {t('Contact_Manager')} - + {data?.user.username && } diff --git a/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx b/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx index ac3d45edb8ed..48f26d317397 100644 --- a/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx +++ b/apps/meteor/client/views/omnichannel/directory/ContactContextualBar.tsx @@ -19,7 +19,7 @@ const ContactContextualBar = () => { }; if (context === 'new') { - return ; + return ; } if (context === 'edit') { diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx index 4f8da207fa4f..1747400c30e2 100644 --- a/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx +++ b/apps/meteor/client/views/omnichannel/directory/contacts/ContactTable.tsx @@ -2,7 +2,6 @@ import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitl import { useDebouncedState, useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import { hashQueryKey } from '@tanstack/react-query'; -import type { ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; import FilterByText from '../../../../components/FilterByText'; @@ -24,19 +23,19 @@ import { parseOutboundPhoneNumber } from '../../../../lib/voip/parseOutboundPhon import { CallDialpadButton } from '../components/CallDialpadButton'; import { useCurrentContacts } from './hooks/useCurrentContacts'; -function ContactTable(): ReactElement { +function ContactTable() { + const t = useTranslation(); + const { current, itemsPerPage, setItemsPerPage, setCurrent, ...paginationProps } = usePagination(); - const { sortBy, sortDirection, setSort } = useSort<'username' | 'phone' | 'name' | 'visitorEmails.address' | 'lastChat.ts'>('username'); + const { sortBy, sortDirection, setSort } = useSort<'name' | 'phone' | 'visitorEmails.address' | 'lastChat.ts'>('name'); const isCallReady = useIsCallReady(); const [term, setTerm] = useDebouncedState('', 500); - const t = useTranslation(); - const query = useDebouncedValue( useMemo( () => ({ - term, + searchText: term, sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }), @@ -72,9 +71,6 @@ function ContactTable(): ReactElement { const headers = ( <> - - {t('Username')} - {t('Name')} @@ -105,7 +101,7 @@ function ContactTable(): ReactElement { return ( <> - {((isSuccess && data?.visitors.length > 0) || queryHasChanged) && ( + {((isSuccess && data?.contacts.length > 0) || queryHasChanged) && (