Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const contactOneMock = createFakeContactWithManagerData({
_id: 'contact-1',
name: 'Contact 1',
phones: [{ phoneNumber: recipientOnePhoneNumber }],
unknown: false,
channels: [
createFakeContactChannel({
name: 'provider-1',
Expand All @@ -29,6 +30,7 @@ const contactTwoMock = createFakeContactWithManagerData({
_id: 'contact-2',
name: 'Contact 2',
phones: [{ phoneNumber: recipientTwoPhoneNumber }],
unknown: false,
channels: [
createFakeContactChannel({
name: 'provider-2',
Expand Down Expand Up @@ -163,6 +165,17 @@ describe('RecipientForm', () => {
expect(retryButton).toBeInTheDocument();
});

it('should show retry button when contact is unknown', async () => {
getContactMock.mockImplementationOnce(() => ({ contact: createFakeContactWithManagerData({ _id: 'contact-1', unknown: true }) }));

render(<RecipientForm defaultValues={{ contactId: 'contact-1' }} {...defaultProps} />, { wrapper: appRoot.build() });

await waitFor(() => expect(screen.getByLabelText('Contact*')).toHaveAccessibleDescription('Error loading contact information'));

const retryButton = screen.getByRole('button', { name: 'Retry' });
expect(retryButton).toBeInTheDocument();
});

it('should disable recipient field when contact fetch fails', async () => {
getContactMock.mockImplementationOnce(() => Promise.reject());
const defaultValues = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,16 @@ const RecipientForm = (props: RecipientFormProps) => {
refetch: refetchContact,
} = useQuery({
queryKey: omnichannelQueryKeys.contact(contactId),
queryFn: () => getContact({ contactId }),
queryFn: async () => {
const data = await getContact({ contactId });

// TODO: Can be safely removed once unknown contacts handling is added to the endpoint
if (data?.contact && data.contact.unknown) {
throw new ContactNotFoundError();
}

return data;
},
staleTime: 5 * 60 * 1000,
select: (data) => data?.contact || undefined,
enabled: !!contactId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,7 @@ const ContactInfo = ({ contact, onClose }: ContactInfoProps) => {
const formatDate = useFormatDate();
const canEditContact = usePermission('edit-omnichannel-contact');

const {
_id: contactId,
name,
emails,
phones,
conflictingFields,
createdAt,
lastChat,
contactManager,
customFields: userCustomFields,
} = contact;
const { name, emails, phones, conflictingFields, createdAt, lastChat, contactManager, customFields: userCustomFields } = contact;

const hasConflicts = conflictingFields && conflictingFields?.length > 0;
const customFieldEntries = useValidCustomFields(userCustomFields);
Expand Down Expand Up @@ -106,15 +96,15 @@ const ContactInfo = ({ contact, onClose }: ContactInfoProps) => {
</Tabs>
{context === 'details' && (
<ContactInfoDetails
contactId={contactId}
contact={contact}
createdAt={createdAt}
contactManager={contactManager}
phones={phones?.map(({ phoneNumber }) => phoneNumber)}
emails={emails?.map(({ address }) => address)}
customFieldEntries={customFieldEntries}
/>
)}
{context === 'channels' && <ContactInfoChannels contactId={contact?._id} />}
{context === 'channels' && <ContactInfoChannels contact={contact} />}
{context === 'history' && <ContactInfoHistory contact={contact} />}
</ContextualbarDialog>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import { VirtualizedScrollbars } from '../../../../../components/CustomScrollbar
import useOutboundProvidersList from '../../../../../components/Omnichannel/OutboundMessage/hooks/useOutboundProvidersList';

type ContactInfoChannelsProps = {
contactId: ILivechatContact['_id'];
contact: Pick<ILivechatContact, '_id' | 'unknown'>;
};

const ContactInfoChannels = ({ contactId }: ContactInfoChannelsProps) => {
const ContactInfoChannels = ({ contact }: ContactInfoChannelsProps) => {
const { t } = useTranslation();

const getContactChannels = useEndpoint('GET', '/v1/omnichannel/contacts.channels');
const { data, isError, isPending } = useQuery({
queryKey: ['getContactChannels', contactId],
queryFn: () => getContactChannels({ contactId }),
queryKey: ['getContactChannels', contact._id],
queryFn: () => getContactChannels({ contactId: contact._id }),
});

const { data: providers = [] } = useOutboundProvidersList({
Expand Down Expand Up @@ -68,7 +68,7 @@ const ContactInfoChannels = ({ contactId }: ContactInfoChannelsProps) => {
<ContactInfoChannelsItem
key={index}
{...data}
contactId={contactId}
contact={contact}
canSendOutboundMessage={data.details.id ? providers.includes(data.details.id) : false}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ILivechatContactChannel, Serialized } from '@rocket.chat/core-typings';
import type { ILivechatContact, ILivechatContactChannel, Serialized } from '@rocket.chat/core-typings';
import { css } from '@rocket.chat/css-in-js';
import { Box, Palette } from '@rocket.chat/fuselage';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
Expand All @@ -13,12 +13,12 @@ import { useTimeFromNow } from '../../../../../hooks/useTimeFromNow';
import { useOmnichannelSource } from '../../../hooks/useOmnichannelSource';

type ContactInfoChannelsItemProps = Serialized<ILivechatContactChannel> & {
contactId?: string;
contact?: Pick<ILivechatContact, '_id' | 'unknown'>;
canSendOutboundMessage?: boolean;
};

const ContactInfoChannelsItem = ({
contactId,
contact,
visitor,
details,
blocked,
Expand Down Expand Up @@ -55,13 +55,15 @@ const ContactInfoChannelsItem = ({
items.unshift({
id: 'outbound-message',
icon: 'send',
disabled: contact?.unknown,
tooltip: contact?.unknown ? t('error-unknown-contact') : undefined,
content: t('Outbound_message'),
onClick: () => outboundMessageModal.open({ contactId, providerId: details.id }),
onClick: () => outboundMessageModal.open({ contactId: contact?._id, providerId: details.id }),
});
}

return items;
}, [blocked, canSendOutboundMessage, contactId, details.id, handleBlockContact, outboundMessageModal, t]);
}, [blocked, canSendOutboundMessage, contact?._id, contact?.unknown, details.id, handleBlockContact, outboundMessageModal, t]);

return (
<Box
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILivechatContact } from '@rocket.chat/core-typings';
import { Divider, Margins } from '@rocket.chat/fuselage';
import { useTranslation } from 'react-i18next';

Expand All @@ -12,24 +13,24 @@ import Info from '../../../components/Info';
import Label from '../../../components/Label';

type ContactInfoDetailsProps = {
contactId: string;
contact: Pick<ILivechatContact, '_id' | 'unknown'>;
emails?: string[];
phones?: string[];
createdAt: string;
customFieldEntries: [string, string | unknown][];
contactManager?: string;
};

const ContactInfoDetails = ({ contactId, emails, phones, createdAt, customFieldEntries, contactManager }: ContactInfoDetailsProps) => {
const ContactInfoDetails = ({ contact, emails, phones, createdAt, customFieldEntries, contactManager }: ContactInfoDetailsProps) => {
const { t } = useTranslation();
const formatDate = useFormatDate();

return (
<ContextualbarScrollableContent>
{emails?.length ? (
<Field>
<Label id={`${contactId}-emails`}>{t('Email')}</Label>
<ul aria-labelledby={`${contactId}-emails`}>
<Label id={`${contact._id}-emails`}>{t('Email')}</Label>
<ul aria-labelledby={`${contact._id}-emails`}>
{emails.map((email) => (
<ContactInfoDetailsEntry key={email} is='li' icon='mail' value={email} />
))}
Expand All @@ -39,10 +40,10 @@ const ContactInfoDetails = ({ contactId, emails, phones, createdAt, customFieldE

{phones?.length ? (
<Field>
<Label id={`${contactId}-phones`}>{t('Phone')}</Label>
<ul aria-labelledby={`${contactId}-phones`}>
<Label id={`${contact._id}-phones`}>{t('Phone')}</Label>
<ul aria-labelledby={`${contact._id}-phones`}>
{phones.map((phone) => (
<ContactInfoPhoneEntry key={phone} is='li' contactId={contactId} value={phone} />
<ContactInfoPhoneEntry key={phone} is='li' contact={contact} value={phone} />
))}
</ul>
</Field>
Expand All @@ -53,8 +54,8 @@ const ContactInfoDetails = ({ contactId, emails, phones, createdAt, customFieldE
<Margins block={4}>
{createdAt && (
<Field>
<Label id={`${contactId}-created-at`}>{t('Created_at')}</Label>
<Info aria-labelledby={`${contactId}-created-at`}>{formatDate(createdAt)}</Info>
<Label id={`${contact._id}-created-at`}>{t('Created_at')}</Label>
<Info aria-labelledby={`${contact._id}-created-at`}>{formatDate(createdAt)}</Info>
</Field>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { useOutboundMessageModal } from '../../../../../components/Omnichannel/O
import { useHasLicenseModule } from '../../../../../hooks/useHasLicenseModule';

type ContactInfoOutboundMessageButtonProps = {
title?: string;
disabled?: boolean;
defaultValues?: OutboundMessageModalProps['defaultValues'];
};

const ContactInfoOutboundMessageButton = ({ defaultValues }: ContactInfoOutboundMessageButtonProps) => {
const ContactInfoOutboundMessageButton = ({ defaultValues, disabled, title }: ContactInfoOutboundMessageButtonProps) => {
const { t } = useTranslation();
const outboundMessageModal = useOutboundMessageModal();

Expand All @@ -21,7 +23,15 @@ const ContactInfoOutboundMessageButton = ({ defaultValues }: ContactInfoOutbound
return null;
}

return <IconButton onClick={() => outboundMessageModal.open(defaultValues)} tiny icon='send' title={t('Outbound_message')} />;
return (
<IconButton
disabled={disabled}
onClick={() => outboundMessageModal.open(defaultValues)}
tiny
icon='send'
title={`${t('Outbound_message')} ${title ? `(${title})` : ''}`}
/>
);
};

export default ContactInfoOutboundMessageButton;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ILivechatContact } from '@rocket.chat/core-typings';
import { IconButton } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -10,10 +11,10 @@ import useClipboardWithToast from '../../../../../hooks/useClipboardWithToast';
import { parseOutboundPhoneNumber } from '../../../../../lib/voip/parseOutboundPhoneNumber';

type ContactInfoPhoneEntryProps = Omit<ComponentProps<typeof ContactInfoDetailsEntry>, 'icon' | 'actions'> & {
contactId?: string;
contact?: Pick<ILivechatContact, '_id' | 'unknown'>;
};

const ContactInfoPhoneEntry = ({ contactId, value, ...props }: ContactInfoPhoneEntryProps) => {
const ContactInfoPhoneEntry = ({ contact, value, ...props }: ContactInfoPhoneEntryProps) => {
const { t } = useTranslation();
const isCallReady = useIsCallReady();
const { copy } = useClipboardWithToast(value);
Expand All @@ -26,7 +27,12 @@ const ContactInfoPhoneEntry = ({ contactId, value, ...props }: ContactInfoPhoneE
actions={[
<IconButton key={`${value}-copy`} onClick={() => copy()} tiny icon='copy' title={t('Copy')} />,
isCallReady ? <ContactInfoCallButton key={`${value}-call`} phoneNumber={value} /> : null,
<ContactInfoOutboundMessageButton key={`${value}-outbound-message`} defaultValues={{ contactId, recipient: value }} />,
<ContactInfoOutboundMessageButton
key={`${value}-outbound-message`}
title={contact?.unknown ? t('error-unknown-contact') : undefined}
disabled={contact?.unknown}
defaultValues={{ contactId: contact?._id, recipient: value }}
/>,
]}
/>
);
Expand Down
Loading