Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3597234
chore: adds new action column to contact center
lucas-a-pelegrino Aug 8, 2025
38cad53
tests: adds unit testing for disableContactById
lucas-a-pelegrino Aug 1, 2025
a290736
feat: adds new permission and setting to handle livechat contact removal
lucas-a-pelegrino Aug 5, 2025
c47f76e
tests: adds setting check for unit/e2e tests
lucas-a-pelegrino Aug 5, 2025
98f9a5d
chore: addresses pull request requested changes
lucas-a-pelegrino Aug 11, 2025
974c154
chore: addresses further PR requested changes
lucas-a-pelegrino Aug 11, 2025
a2acef2
tests: moves disableContact.spec.ts to tests/unit/
lucas-a-pelegrino Aug 12, 2025
c9774de
feat: adds kebab menu to contact center for contact removal
lucas-a-pelegrino Aug 15, 2025
38f843f
feat: adds the edit option on the kebab menu as well as some improvem…
lucas-a-pelegrino Aug 18, 2025
4092baa
feat: adds minor improvements to kebab menu options build
lucas-a-pelegrino Aug 18, 2025
4aab973
fix: toast message not showing up
lucas-a-pelegrino Aug 20, 2025
8cace9b
chore: adds minor improvements to RemoveContactModal
lucas-a-pelegrino Aug 25, 2025
4a88529
tests: adds UI tests for contact removal
lucas-a-pelegrino Aug 25, 2025
85704cc
fix: i18n label for contact removal confirmation
lucas-a-pelegrino Aug 25, 2025
9b7474b
fix: updates text aria label getter
lucas-a-pelegrino Aug 25, 2025
49c9818
chore: applies code review requested changes
lucas-a-pelegrino Aug 25, 2025
c48a175
fix: removes .only from test
lucas-a-pelegrino Aug 25, 2025
4a6ed13
tests: adds a test assertion that validates input is correctly parsin…
lucas-a-pelegrino Aug 26, 2025
4241012
chore: adds minor improvements to contact removal confirmation logic
lucas-a-pelegrino Aug 27, 2025
e5ec42e
tests: minor improvements to contact center test setup hooks
lucas-a-pelegrino Aug 27, 2025
76c916c
fix: linting error
lucas-a-pelegrino Aug 27, 2025
97a6ace
fix: error messages not displaying on toast
lucas-a-pelegrino Aug 29, 2025
e9d7243
Merge branch 'develop' into feat/CTZ-173-FE
lucas-a-pelegrino Aug 29, 2025
54773e1
Merge branch 'develop' into feat/CTZ-173-FE
kodiakhq[bot] Sep 1, 2025
5e60e26
Merge branch 'develop' into feat/CTZ-173-FE
kodiakhq[bot] Sep 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { ILivechatContactChannel } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { GenericMenuItemProps } from '@rocket.chat/ui-client';
import { GenericMenu } from '@rocket.chat/ui-client';
import { useRouter, useSetModal, usePermission } from '@rocket.chat/ui-contexts';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';

import RemoveContactModal from './RemoveContactModal';

type ContactItemMenuProps = {
_id: string;
name: string;
channels: ILivechatContactChannel[];
};

const ContactItemMenu = ({ _id, name, channels }: ContactItemMenuProps): ReactElement => {
const { t } = useTranslation();
const setModal = useSetModal();
const router = useRouter();

const canEditContact = usePermission('update-livechat-contact');
const canDeleteContact = usePermission('delete-livechat-contact');

const handleContactEdit = useEffectEvent((): void =>
router.navigate({
pattern: '/omnichannel-directory/:tab?/:context?/:id?',
params: {
tab: 'contacts',
context: 'edit',
id: _id,
},
}),
);

const handleContactRemoval = useEffectEvent(() => {
setModal(<RemoveContactModal _id={_id} name={name} channelsCount={channels.length} onClose={() => setModal(null)} />);
});

const menuOptions: GenericMenuItemProps[] = [
{
id: 'edit',
icon: 'edit',
content: t('Edit'),
onClick: () => handleContactEdit(),
disabled: !canEditContact,
},
{
id: 'delete',
icon: 'trash',
content: t('Delete'),
onClick: () => handleContactRemoval(),
variant: 'danger',
disabled: !canDeleteContact,
},
];

return <GenericMenu detached title={t('More_actions')} sections={[{ title: '', items: menuOptions }]} placement='bottom-end' />;
};

export default ContactItemMenu;
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function ContactTable() {
{t('Last_Chat')}
</GenericTableHeaderCell>
{isCallReady && <GenericTableHeaderCell key='call' width={44} />}
<GenericTableHeaderCell key='spacer' w={40} />
</>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import type { ILivechatContactWithManagerData } from '@rocket.chat/rest-typings';
import { useRoute } from '@rocket.chat/ui-contexts';

import ContactItemMenu from './ContactItemMenu';
import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable';
import { OmnichannelRoomIcon } from '../../../../components/RoomIcon/OmnichannelRoomIcon';
import { useIsCallReady } from '../../../../contexts/CallContext';
Expand Down Expand Up @@ -63,6 +64,9 @@ const ContactTableRow = ({ _id, name, phones, contactManager, lastChat, channels
<CallDialpadButton phoneNumber={phoneNumber} />
</GenericTableCell>
)}
<GenericTableCell>
<ContactItemMenu _id={_id} name={name} channels={channels} />
</GenericTableCell>
</GenericTableRow>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Box, Input } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { ReactElement, ChangeEvent } from 'react';
import { useState, useId } from 'react';
import { useTranslation } from 'react-i18next';

type RemoveContactModalProps = {
_id: string;
name: string;
channelsCount: number;
onClose: () => void;
};

const RemoveContactModal = ({ _id, name, channelsCount, onClose }: RemoveContactModalProps): ReactElement => {
const { t } = useTranslation();
const [text, setText] = useState<string>('');

const queryClient = useQueryClient();
const removeContact = useEndpoint('POST', '/v1/omnichannel/contacts.delete');
const dispatchToast = useToastMessageDispatch();
const contactDeleteModalId = useId();

const handleSubmit = useEffectEvent((event: ChangeEvent<HTMLFormElement>): void => {
event.preventDefault();
removeContactMutation.mutate();
});

const confirmationText = t('Delete').toLowerCase();

const removeContactMutation = useMutation({
mutationFn: () => removeContact({ contactId: _id }),
onSuccess: async () => {
dispatchToast({ type: 'success', message: t('Contact_has_been_deleted') });
await Promise.all([
queryClient.invalidateQueries({
queryKey: ['current-contacts'],
}),
queryClient.invalidateQueries({
queryKey: ['getContactsByIds', _id],
}),
]);
onClose();
},
onError: (error) => {
dispatchToast({ type: 'error', message: error });
onClose();
},
});

return (
<GenericModal
wrapperFunction={(props) => <Box is='form' onSubmit={handleSubmit} {...props} />}
onCancel={onClose}
confirmText={t('Delete')}
title={t('Delete_Contact')}
onClose={onClose}
variant='danger'
confirmDisabled={text !== confirmationText}
>
<Box is='p' id={`${contactDeleteModalId}-description`} mbe={16}>
{t('Are_you_sure_delete_contact', { contactName: name, channelsCount, confirmationText })}
</Box>
<Box mbe={16} display='flex' justifyContent='stretch'>
<Input
value={text}
name='confirmContactRemoval'
aria-label={t('Confirm_contact_removal')}
aria-describedby={`${contactDeleteModalId}-description`}
onChange={(event: ChangeEvent<HTMLInputElement>) => setText(event.currentTarget.value)}
/>
</Box>
</GenericModal>
);
};

export default RemoveContactModal;
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const EXISTING_CONTACT = {
phones: [faker.phone.number('+############')],
token: undefined,
};
const DELETE_CONTACT = {
id: undefined,
name: `${faker.person.firstName()} ${faker.person.lastName()}`,
emails: [faker.internet.email().toLowerCase()],
phones: [faker.phone.number('+############')],
};

const NEW_CUSTOM_FIELD = {
searchable: true,
field: 'hiddenCustomField',
Expand Down Expand Up @@ -63,18 +70,17 @@ test.describe('Omnichannel Contact Center', () => {
let poOmniSection: OmnichannelSection;

test.beforeAll(async ({ api }) => {
// Add a contact
await api.post('/omnichannel/contacts', EXISTING_CONTACT);
// Add contacts
await Promise.all([api.post('/omnichannel/contacts', EXISTING_CONTACT), api.post('/omnichannel/contacts', DELETE_CONTACT)]);

if (IS_EE) {
await api.post('/livechat/custom.field', NEW_CUSTOM_FIELD);
}
});

test.afterAll(async ({ api }) => {
// Remove added contact
await api.delete(`/livechat/visitor/${NEW_CONTACT.token}`);
await api.delete(`/livechat/visitor/${EXISTING_CONTACT.token}`);
// Remove added contacts
await Promise.all([api.delete(`/livechat/visitor/${NEW_CONTACT.token}`), api.delete(`/livechat/visitor/${EXISTING_CONTACT.token}`)]);

if (IS_EE) {
await api.post('method.call/livechat:removeCustomField', { message: NEW_CUSTOM_FIELD.field });
Expand Down Expand Up @@ -262,4 +268,33 @@ test.describe('Omnichannel Contact Center', () => {
await expect(poContacts.findRowByName(EDIT_CONTACT.name)).toBeVisible();
});
});

test('Delete a contact', async () => {
await test.step('Find contact and open modal', async () => {
await poContacts.inputSearch.fill(DELETE_CONTACT.name);
await poContacts.findRowMenu(DELETE_CONTACT.name).click();
await poContacts.findMenuItem('Delete').click();
});

await test.step('Fill confirmation and delete contact', async () => {
await expect(poContacts.deleteContactModal).toBeVisible();
await expect(poContacts.btnDeleteContact).toBeDisabled();

// Fills the input with the wrong confirmation
await poContacts.inputDeleteContactConfirmation.fill('wrong');
await expect(poContacts.btnDeleteContact).toBeDisabled();

// Fills the input correctly
await poContacts.inputDeleteContactConfirmation.fill('delete');
await expect(poContacts.btnDeleteContact).toBeEnabled();
await poContacts.btnDeleteContact.click();

await expect(poContacts.deleteContactModal).not.toBeVisible();
});

await test.step('Confirm contact removal', async () => {
await poContacts.inputSearch.fill(DELETE_CONTACT.name);
await expect(poContacts.findRowByName(DELETE_CONTACT.name)).not.toBeVisible();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,27 @@ export class OmnichannelContacts {
}

findRowByName(contactName: string) {
return this.page.locator(`td >> text="${contactName}"`);
return this.page.locator('tr', { has: this.page.locator(`td >> text="${contactName}"`) });
}

findRowMenu(contactName: string): Locator {
return this.findRowByName(contactName).getByRole('button', { name: 'More Actions' });
}

findMenuItem(name: string): Locator {
return this.page.getByRole('menuitem', { name });
}

get deleteContactModal(): Locator {
return this.page.getByRole('dialog', { name: 'Delete Contact' });
}

get inputDeleteContactConfirmation(): Locator {
return this.deleteContactModal.getByRole('textbox', { name: 'Confirm contact removal' });
}

get btnDeleteContact(): Locator {
return this.deleteContactModal.getByRole('button', { name: 'Delete' });
}

get btnFilters(): Locator {
Expand Down
7 changes: 7 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,7 @@
"Are_you_sure_you_want_to_disable_Facebook_integration": "Are you sure you want to disable Facebook integration?",
"Are_you_sure_you_want_to_pin_this_message": "Are you sure you want to pin this message?",
"Are_you_sure_you_want_to_reset_the_name_of_all_priorities": "Are you sure you want to reset the name of all priorities?",
"Are_you_sure_delete_contact": "Are you sure you want to delete {{contactName}} and all {{channelsCount}} of their conversation history? To confirm, type '{{confirmationText}}' in the field below.",
"Ask_enable_advanced_contact_profile": "Ask your workspace admin to enable advanced contact profile",
"Asset_preview": "Asset preview",
"Assets": "Assets",
Expand Down Expand Up @@ -1134,6 +1135,7 @@
"Confirm_new_workspace_description": "Identification data and cloud connection data will be reset.<br/><br/><strong>Warning</strong>: License can be affected if changing workspace URL.",
"Confirm_password": "Confirm password",
"Confirm_your_password": "Confirm your password",
"Confirm_contact_removal": "Confirm Contact Removal",
"Confirmation": "Confirmation",
"Conflicts_found": "Conflicts found",
"Connect": "Connect",
Expand All @@ -1158,6 +1160,7 @@
"Contact_email": "Contact email",
"Contact_has_been_created": "Contact has been created",
"Contact_has_been_updated": "Contact has been updated",
"Contact_has_been_deleted": "Contact has been deleted",
"Contact_history_is_preserved": "Contact history is preserved",
"Contact_identification": "Contact identification",
"Contact_not_found": "Contact not found",
Expand Down Expand Up @@ -1593,6 +1596,7 @@
"Default_value": "Default value",
"Delete": "Delete",
"Delete_Department?": "Delete Department?",
"Delete_Contact": "Delete Contact",
"Delete_File_Warning": "Deleting a file will delete it forever. This cannot be undone.",
"Delete_Role_Warning": "This cannot be undone",
"Delete_Role_Warning_Not_Enterprise": "This cannot be undone. You won't be able to create a new custom role, since that feature is no longer available for your current plan.",
Expand Down Expand Up @@ -6045,6 +6049,9 @@
"error-channels-setdefault-missing-default-param": "The bodyParam 'default' is required",
"error-comment-is-required": "Comment is required",
"error-contact-sent-last-message-so-cannot-place-on-hold": "You cannot place chat on-hold, when the Contact has sent the last message",
"error-contact-not-found": "Contact not found.",
"error-contact-has-open-rooms": "Cannot delete contact with open rooms.",
"error-contact-something-went-wrong": "Something went wrong while deleting the contact.",
"error-could-not-change-email": "Could not change email",
"error-could-not-change-name": "Could not change name",
"error-could-not-change-username": "Could not change username",
Expand Down
Loading