diff --git a/apps/meteor/client/components/CustomFieldsForm.js b/apps/meteor/client/components/CustomFieldsForm.js
index 5fc7af74187..b4c5d131716 100644
--- a/apps/meteor/client/components/CustomFieldsForm.js
+++ b/apps/meteor/client/components/CustomFieldsForm.js
@@ -113,7 +113,7 @@ const CustomFieldsAssembler = ({ formValues, formHandlers, customFields, ...prop
});
export default function CustomFieldsForm({
- jsonCustomFields = undefined,
+ jsonCustomFields = {},
customFieldsData,
setCustomFieldsData,
onLoadFields = () => {},
@@ -129,7 +129,7 @@ export default function CustomFieldsForm({
}
});
- const hasCustomFields = Boolean(Object.values(customFields).length);
+ const hasCustomFields = useMemo(() => Object.values(customFields).length > 0, [customFields]);
const defaultFields = useMemo(
() =>
Object.entries(customFields).reduce((data, [key, value]) => {
@@ -142,11 +142,14 @@ export default function CustomFieldsForm({
const { values, handlers } = useForm({ ...defaultFields, ...customFieldsData });
useEffect(() => {
- onLoadFields(hasCustomFields);
+ onLoadFields?.(hasCustomFields);
+ }, [onLoadFields, hasCustomFields]);
+
+ useEffect(() => {
if (hasCustomFields) {
setCustomFieldsData(values);
}
- }, [hasCustomFields, onLoadFields, setCustomFieldsData, values]);
+ }, [hasCustomFields, setCustomFieldsData, values]);
if (!hasCustomFields) {
return null;
diff --git a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx
index 96a70c8ffbc..15d17047bd9 100644
--- a/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx
+++ b/apps/meteor/client/sidebar/sections/OmnichannelSection.tsx
@@ -41,7 +41,12 @@ const OmnichannelSection = (props: typeof Box): ReactElement => {
{isCallEnabled && }
{hasPermissionToSeeContactCenter && (
- handleRoute('directory')} />
+ handleRoute('directory')}
+ />
)}
{isCallReady && }
diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js
deleted file mode 100644
index 97503037619..00000000000
--- a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js
+++ /dev/null
@@ -1,255 +0,0 @@
-import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useState, useMemo, useEffect } from 'react';
-
-import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client';
-import { validateEmail } from '../../../../../../lib/emailValidator';
-import CustomFieldsForm from '../../../../../components/CustomFieldsForm';
-import VerticalBar from '../../../../../components/VerticalBar';
-import { AsyncStatePhase } from '../../../../../hooks/useAsyncState';
-import { useComponentDidUpdate } from '../../../../../hooks/useComponentDidUpdate';
-import { useEndpointData } from '../../../../../hooks/useEndpointData';
-import { useForm } from '../../../../../hooks/useForm';
-import { createToken } from '../../../../../lib/utils/createToken';
-import { useFormsSubscription } from '../../../additionalForms';
-import { FormSkeleton } from '../../Skeleton';
-
-const initialValues = {
- token: '',
- name: '',
- email: '',
- phone: '',
- username: '',
-};
-
-const getInitialValues = (data) => {
- if (!data) {
- return initialValues;
- }
-
- const {
- contact: { name, token, phone, visitorEmails, livechatData, contactManager },
- } = data;
-
- return {
- token: token ?? '',
- name: name ?? '',
- email: visitorEmails ? visitorEmails[0].address : '',
- phone: phone ? phone[0].phoneNumber : '',
- livechatData: livechatData ?? {},
- username: contactManager?.username ?? '',
- };
-};
-
-function ContactNewEdit({ id, data, close }) {
- const t = useTranslation();
-
- const canViewCustomFields = () => hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']);
-
- const initialValue = getInitialValues(data);
-
- const { username: initialUsername } = initialValue;
-
- const { values, handlers, hasUnsavedChanges: hasUnsavedChangesContact } = useForm(initialValue);
-
- const eeForms = useFormsSubscription();
-
- const { useContactManager = () => {} } = eeForms;
-
- const ContactManager = useContactManager();
-
- const { handleName, handleEmail, handlePhone, handleUsername } = handlers;
- const { token, name, email, phone, username } = values;
-
- const {
- values: valueCustom,
- handlers: handleValueCustom,
- hasUnsavedChanges: hasUnsavedChangesCustomFields,
- } = useForm({
- livechatData: values.livechatData,
- });
-
- const { handleLivechatData } = handleValueCustom;
- const { livechatData } = valueCustom;
-
- const [nameError, setNameError] = useState();
- const [emailError, setEmailError] = useState();
- const [phoneError, setPhoneError] = useState();
- const [customFieldsError, setCustomFieldsError] = useState([]);
- const [userId, setUserId] = useState('no-agent-selected');
-
- const { value: allCustomFields, phase: state } = useEndpointData('/v1/livechat/custom-fields');
-
- const jsonConverterToValidFormat = (customFields) => {
- const jsonObj = {};
- customFields.forEach(({ _id, label, visibility, options, scope, defaultValue, required }) => {
- (visibility === 'visible') & (scope === 'visitor') &&
- (jsonObj[_id] = {
- label,
- type: options ? 'select' : 'text',
- required,
- defaultValue,
- options: options && options.split(',').map((item) => item.trim()),
- });
- });
- return jsonObj;
- };
-
- const jsonCustomField = useMemo(
- () => (allCustomFields && allCustomFields.customFields ? jsonConverterToValidFormat(allCustomFields.customFields) : {}),
- [allCustomFields],
- );
-
- const saveContact = useEndpoint('POST', '/v1/omnichannel/contact');
- const emailAlreadyExistsAction = useEndpoint('GET', '/v1/omnichannel/contact.search');
- const phoneAlreadyExistsAction = useEndpoint('GET', '/v1/omnichannel/contact.search');
- const getUserData = useEndpoint('GET', '/v1/users.info');
-
- const checkEmailExists = useMutableCallback(async () => {
- if (!validateEmail(email)) {
- return;
- }
- const { contact } = await emailAlreadyExistsAction({ email });
- if (!contact || (id && contact._id === id)) {
- return setEmailError(null);
- }
- setEmailError(t('Email_already_exists'));
- });
-
- const checkPhoneExists = useMutableCallback(async () => {
- if (!phone) {
- return;
- }
- const { contact } = await phoneAlreadyExistsAction({ phone });
- if (!contact || (id && contact._id === id)) {
- return setPhoneError(null);
- }
- setPhoneError(t('Phone_already_exists'));
- });
-
- const dispatchToastMessage = useToastMessageDispatch();
-
- useComponentDidUpdate(() => {
- setNameError(!name ? t('The_field_is_required', t('Name')) : '');
- }, [t, name]);
- useComponentDidUpdate(() => {
- setEmailError(email && !validateEmail(email) ? t('Validate_email_address') : null);
- }, [t, email]);
- useComponentDidUpdate(() => {
- !phone && setPhoneError(null);
- }, [phone]);
-
- useEffect(() => {
- if (!initialUsername) {
- return;
- }
-
- getUserData({ username: initialUsername }).then(({ user }) => {
- setUserId(user._id);
- });
- }, [getUserData, initialUsername]);
-
- const handleContactManagerChange = useMutableCallback(async (userId) => {
- setUserId(userId);
- if (userId === 'no-agent-selected') {
- handleUsername('');
- return;
- }
-
- getUserData({ userId }).then(({ user }) => {
- handleUsername(user.username);
- });
- });
-
- const handleSave = useMutableCallback(async (e) => {
- e.preventDefault();
- let error = false;
- if (!name) {
- setNameError(t('The_field_is_required', 'name'));
- error = true;
- }
- if (email && !validateEmail(email)) {
- setEmailError(t('Validate_email_address'));
- error = true;
- }
-
- if (error) {
- return;
- }
-
- const payload = {
- name,
- phone,
- email,
- customFields: livechatData || {},
- token: token || createToken(),
- ...(username && { contactManager: { username } }),
- ...(id && { _id: id }),
- };
-
- try {
- await saveContact(payload);
- dispatchToastMessage({ type: 'success', message: t('Saved') });
- close();
- } catch (error) {
- dispatchToastMessage({ type: 'error', message: error });
- }
- });
-
- const formIsValid =
- (hasUnsavedChangesContact || hasUnsavedChangesCustomFields) && name && !emailError && !phoneError && customFieldsError.length === 0;
-
- if ([state].includes(AsyncStatePhase.LOADING)) {
- return ;
- }
-
- return (
- <>
-
-
- {t('Name')}*
-
-
-
- {nameError}
-
-
- {t('Email')}
-
-
-
- {t(emailError)}
-
-
- {t('Phone')}
-
-
-
- {t(phoneError)}
-
- {canViewCustomFields() && allCustomFields && (
-
- )}
- {ContactManager && }
-
-
-
-
-
-
-
- >
- );
-}
-
-export default ContactNewEdit;
diff --git a/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx
new file mode 100644
index 00000000000..27cb9daeba0
--- /dev/null
+++ b/apps/meteor/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.tsx
@@ -0,0 +1,262 @@
+import { ILivechatCustomField, ILivechatVisitor, Serialized } from '@rocket.chat/core-typings';
+import { Field, TextInput, ButtonGroup, Button } from '@rocket.chat/fuselage';
+import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
+import { useQuery } from '@tanstack/react-query';
+import React, { useState, useEffect, ReactElement } from 'react';
+import { useController, useForm } from 'react-hook-form';
+import { debounce } from 'underscore';
+
+import { hasAtLeastOnePermission } from '../../../../../../app/authorization/client';
+import { validateEmail } from '../../../../../../lib/emailValidator';
+import CustomFieldsForm from '../../../../../components/CustomFieldsForm';
+import VerticalBar from '../../../../../components/VerticalBar';
+import { createToken } from '../../../../../lib/utils/createToken';
+import { useFormsSubscription } from '../../../additionalForms';
+import { FormSkeleton } from '../../Skeleton';
+
+type ContactNewEditProps = {
+ id: string;
+ data: { contact: Serialized | null };
+ close(): void;
+};
+
+type ContactFormData = {
+ token: string;
+ name: string;
+ email: string;
+ phone: string;
+ username: string;
+ customFields: Record;
+};
+
+type CustomFieldsMetadata = Record<
+ string,
+ {
+ label: string;
+ type: 'select' | 'text';
+ required?: boolean;
+ defaultValue?: unknown;
+ options?: string[];
+ }
+>;
+
+const DEFAULT_VALUES = {
+ token: '',
+ name: '',
+ email: '',
+ phone: '',
+ username: '',
+ customFields: {},
+};
+
+const getInitialValues = (data: ContactNewEditProps['data']): ContactFormData => {
+ if (!data) {
+ return DEFAULT_VALUES;
+ }
+
+ const { name, token, phone, visitorEmails, livechatData, contactManager } = data.contact ?? {};
+
+ return {
+ token: token ?? '',
+ name: name ?? '',
+ email: visitorEmails ? visitorEmails[0].address : '',
+ phone: phone ? phone[0].phoneNumber : '',
+ customFields: livechatData ?? {},
+ username: contactManager?.username ?? '',
+ };
+};
+
+const formatCustomFieldsMetadata = (customFields: Serialized[]): CustomFieldsMetadata =>
+ customFields
+ .filter(({ visibility, scope }) => visibility === 'visible' && scope === 'visitor')
+ .reduce((obj, { _id, label, options, defaultValue, required }) => {
+ obj[_id] = {
+ label,
+ type: options ? 'select' : 'text',
+ required,
+ defaultValue,
+ options: options?.split(',').map((item) => item.trim()),
+ };
+ return obj;
+ }, {} as CustomFieldsMetadata);
+
+export const ContactNewEdit = ({ id, data, close }: ContactNewEditProps): ReactElement => {
+ const t = useTranslation();
+
+ const canViewCustomFields = (): boolean =>
+ hasAtLeastOnePermission(['view-livechat-room-customfields', 'edit-livechat-room-customfields']);
+
+ const { useContactManager } = useFormsSubscription();
+
+ const ContactManager = useContactManager?.();
+
+ const [userId, setUserId] = useState('no-agent-selected');
+ const dispatchToastMessage = useToastMessageDispatch();
+ const saveContact = useEndpoint('POST', '/v1/omnichannel/contact');
+ const getContactBy = useEndpoint('GET', '/v1/omnichannel/contact.search');
+ const getUserData = useEndpoint('GET', '/v1/users.info');
+ const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields');
+
+ const { data: customFieldsMetadata = {}, isLoading: isLoadingCustomFields } = useQuery(['contact-json-custom-fields'], async () => {
+ const rawFields = await getCustomFields();
+ return formatCustomFieldsMetadata(rawFields?.customFields);
+ });
+
+ const initialValue = getInitialValues(data);
+ const { username: initialUsername } = initialValue;
+
+ const {
+ register,
+ formState: { errors, isValid: isFormValid, isDirty },
+ control,
+ setValue,
+ getValues,
+ handleSubmit,
+ trigger,
+ } = useForm({
+ mode: 'onSubmit',
+ reValidateMode: 'onSubmit',
+ defaultValues: initialValue,
+ });
+
+ const {
+ field: { onChange: handleLivechatData, value: customFields },
+ } = useController({
+ name: 'customFields',
+ control,
+ });
+
+ const [customFieldsErrors, setCustomFieldsErrors] = useState([]);
+ const isValid = isDirty && isFormValid && customFieldsErrors.length === 0;
+
+ useEffect(() => {
+ if (!initialUsername) {
+ return;
+ }
+
+ getUserData({ username: initialUsername }).then(({ user }) => {
+ setUserId(user._id);
+ });
+ }, [getUserData, initialUsername]);
+
+ const isEmailValid = async (email: string): Promise => {
+ if (email === initialValue.email) {
+ return true;
+ }
+
+ if (!validateEmail(email)) {
+ return t('error-invalid-email-address');
+ }
+
+ const { contact } = await getContactBy({ email });
+ return !contact || contact._id === id || t('Email_already_exists');
+ };
+
+ const isPhoneValid = async (phone: string): Promise => {
+ if (!phone || initialValue.phone === phone) {
+ return true;
+ }
+
+ const { contact } = await getContactBy({ phone });
+ return !contact || contact._id === id || t('Phone_already_exists');
+ };
+
+ const isNameValid = (v: string): string | boolean => (!v.trim() ? t('The_field_is_required', t('Name')) : true);
+
+ const handleContactManagerChange = async (userId: string): Promise => {
+ setUserId(userId);
+
+ if (userId === 'no-agent-selected') {
+ setValue('username', '');
+ return;
+ }
+
+ const { user } = await getUserData({ userId });
+ setValue('username', user.username || '');
+ };
+
+ const validate = (fieldName: keyof ContactFormData): (() => void) => debounce(() => trigger(fieldName), 500);
+
+ const handleSave = async (): Promise => {
+ const { name, phone, email, customFields, username, token } = getValues();
+
+ const payload = {
+ name,
+ phone,
+ email,
+ customFields,
+ token: token || createToken(),
+ ...(username && { contactManager: { username } }),
+ ...(id && { _id: id }),
+ };
+
+ try {
+ await saveContact(payload);
+ dispatchToastMessage({ type: 'success', message: t('Saved') });
+ close();
+ } catch (error) {
+ dispatchToastMessage({ type: 'error', message: error });
+ }
+ };
+
+ if (isLoadingCustomFields) {
+ return ;
+ }
+
+ return (
+ <>
+
+
+ {t('Name')}*
+
+
+
+ {errors.name?.message}
+
+
+ {t('Email')}
+
+
+
+ {errors.email?.message}
+
+
+ {t('Phone')}
+
+
+
+ {errors.phone?.message}
+
+ {canViewCustomFields() && customFields && (
+
+ )}
+ {ContactManager && }
+
+
+
+
+
+
+
+ >
+ );
+};
+
+export default ContactNewEdit;
diff --git a/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts b/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts
new file mode 100644
index 00000000000..09f92da299e
--- /dev/null
+++ b/apps/meteor/tests/e2e/omnichannel-contact-center.spec.ts
@@ -0,0 +1,226 @@
+import faker from '@faker-js/faker';
+
+import { OmnichannelContacts } from './page-objects/omnichannel-contacts-list';
+import { OmnichannelSection } from './page-objects/omnichannel-section';
+import { test, expect } from './utils/test';
+import { createToken } from '../../client/lib/utils/createToken';
+
+const createContact = (generateToken = false) => ({
+ id: null,
+ name: `${faker.name.firstName()} ${faker.name.lastName()}`,
+ email: faker.internet.email().toLowerCase(),
+ phone: faker.phone.phoneNumber('+############'),
+ token: generateToken ? createToken() : null,
+ customFields: {},
+});
+
+const NEW_CONTACT = createContact();
+const EDIT_CONTACT = createContact();
+const EXISTING_CONTACT = createContact(true);
+
+const URL = {
+ contactCenter: '/omnichannel-directory/contacts',
+ newContact: '/omnichannel-directory/contacts/new',
+ get editContact() {
+ return `${this.contactCenter}/edit/${NEW_CONTACT.id}`;
+ },
+ get contactInfo() {
+ return `${this.contactCenter}/info/${NEW_CONTACT.id}`;
+ },
+};
+
+const ERROR = {
+ nameRequired: 'The field Name is required.',
+ invalidEmail: 'Invalid email address',
+ existingEmail: 'Email already exists',
+ existingPhone: 'Phone already exists',
+};
+
+test.use({ storageState: 'admin-session.json' });
+
+test.describe('Omnichannel Contact Center', () => {
+ let poContacts: OmnichannelContacts;
+ let poOmniSection: OmnichannelSection;
+
+ test.beforeAll(async ({ api }) => {
+ // Add a contact
+ const { id: _, ...data } = EXISTING_CONTACT;
+ await api.post('/omnichannel/contact', data);
+ });
+
+ test.afterAll(async ({ api }) => {
+ // Remove added contacts
+ await api.delete('/livechat/visitor', { token: EXISTING_CONTACT.token });
+ await api.delete('/livechat/visitor', { token: NEW_CONTACT.token });
+ });
+
+ test.beforeEach(async ({ page }) => {
+ poContacts = new OmnichannelContacts(page);
+ poOmniSection = new OmnichannelSection(page);
+ });
+
+ test.afterEach(async ({ api }) => {
+ await api
+ .get('/omnichannel/contact.search', { phone: NEW_CONTACT.phone })
+ .then((res) => res.json())
+ .then((res) => {
+ NEW_CONTACT.token = res.contact?.token;
+ NEW_CONTACT.id = res.contact?._id;
+ });
+ });
+
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await poOmniSection.btnContactCenter.click();
+ await page.waitForURL(URL.contactCenter);
+ });
+
+ test('Add new contact', async ({ page }) => {
+ await test.step('cancel button', async () => {
+ await poContacts.btnNewContact.click();
+ await page.waitForURL(URL.newContact);
+ await expect(poContacts.newContact.newContactTitle).toBeVisible();
+ await poContacts.newContact.btnCancel.click();
+ await page.waitForURL(URL.contactCenter);
+ await expect(poContacts.newContact.newContactTitle).not.toBeVisible();
+ });
+
+ await test.step('open contextual bar', async () => {
+ await poContacts.btnNewContact.click();
+ await page.waitForURL(URL.newContact);
+ await expect(poContacts.newContact.newContactTitle).toBeVisible();
+ await expect(poContacts.newContact.btnSave).toBeDisabled();
+ });
+
+ await test.step('input name', async () => {
+ await poContacts.newContact.inputName.type(NEW_CONTACT.name);
+ });
+
+ await test.step('validate email format', async () => {
+ await poContacts.newContact.inputEmail.type('invalidemail');
+ await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).toBeVisible();
+ });
+
+ await test.step('validate existing email', async () => {
+ await poContacts.newContact.inputEmail.selectText();
+ await poContacts.newContact.inputEmail.type(EXISTING_CONTACT.email);
+ await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).toBeVisible();
+ await expect(poContacts.newContact.btnSave).toBeDisabled();
+ });
+
+ await test.step('input email', async () => {
+ await poContacts.newContact.inputEmail.selectText();
+ await poContacts.newContact.inputEmail.type(NEW_CONTACT.email);
+ await expect(poContacts.newContact.errorMessage(ERROR.invalidEmail)).not.toBeVisible();
+ await expect(poContacts.newContact.errorMessage(ERROR.existingEmail)).not.toBeVisible();
+ });
+
+ await test.step('validate existing phone ', async () => {
+ await poContacts.newContact.inputPhone.type(EXISTING_CONTACT.phone);
+ await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).toBeVisible();
+ await expect(poContacts.newContact.btnSave).toBeDisabled();
+ });
+
+ await test.step('input phone ', async () => {
+ await poContacts.newContact.inputPhone.selectText();
+ await poContacts.newContact.inputPhone.type(NEW_CONTACT.phone);
+ await expect(poContacts.newContact.errorMessage(ERROR.existingPhone)).not.toBeVisible();
+ });
+
+ await test.step('save new contact ', async () => {
+ await expect(poContacts.newContact.btnSave).toBeEnabled();
+ await poContacts.newContact.btnSave.click();
+ await page.waitForURL(URL.contactCenter);
+ await expect(poContacts.toastSuccess).toBeVisible();
+
+ await poContacts.inputSearch.type(NEW_CONTACT.name);
+ await expect(poContacts.findRowByName(NEW_CONTACT.name)).toBeVisible();
+ });
+ });
+
+ test('Edit new contact', async ({ page }) => {
+ await test.step('search contact and open contextual bar', async () => {
+ await poContacts.inputSearch.type(NEW_CONTACT.name);
+ const row = poContacts.findRowByName(NEW_CONTACT.name);
+ await expect(row).toBeVisible();
+ await row.click();
+ await page.waitForURL(URL.contactInfo);
+ });
+
+ await test.step('cancel button', async () => {
+ await poContacts.contactInfo.btnEdit.click();
+ await page.waitForURL(URL.editContact);
+ await poContacts.contactInfo.btnCancel.click();
+ await page.waitForURL(URL.contactInfo);
+ });
+
+ await test.step('edit contact', async () => {
+ poContacts.contactInfo.btnEdit.click();
+ await page.waitForURL(URL.editContact);
+ });
+
+ await test.step('initial values', async () => {
+ await expect(poContacts.contactInfo.inputName).toHaveValue(NEW_CONTACT.name);
+ await expect(poContacts.contactInfo.inputEmail).toHaveValue(NEW_CONTACT.email);
+ await expect(poContacts.contactInfo.inputPhone).toHaveValue(NEW_CONTACT.phone);
+ });
+
+ await test.step('validate email format', async () => {
+ await poContacts.contactInfo.inputEmail.selectText();
+ await poContacts.contactInfo.inputEmail.type('invalidemail');
+ await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).toBeVisible();
+ });
+
+ await test.step('validate existing email', async () => {
+ await poContacts.contactInfo.inputEmail.selectText();
+ await poContacts.contactInfo.inputEmail.type(EXISTING_CONTACT.email);
+ await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).toBeVisible();
+ await expect(poContacts.contactInfo.btnSave).toBeDisabled();
+ });
+
+ await test.step('input email', async () => {
+ await poContacts.contactInfo.inputEmail.selectText();
+ await poContacts.contactInfo.inputEmail.type(EDIT_CONTACT.email);
+ await expect(poContacts.contactInfo.errorMessage(ERROR.invalidEmail)).not.toBeVisible();
+ await expect(poContacts.contactInfo.errorMessage(ERROR.existingEmail)).not.toBeVisible();
+ await expect(poContacts.contactInfo.btnSave).toBeEnabled();
+ });
+
+ await test.step('validate existing phone ', async () => {
+ await poContacts.contactInfo.inputPhone.selectText();
+ await poContacts.contactInfo.inputPhone.type(EXISTING_CONTACT.phone);
+ await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).toBeVisible();
+ await expect(poContacts.contactInfo.btnSave).toBeDisabled();
+ });
+
+ await test.step('input phone ', async () => {
+ await poContacts.contactInfo.inputPhone.selectText();
+ await poContacts.contactInfo.inputPhone.type(EDIT_CONTACT.phone);
+ await expect(poContacts.contactInfo.errorMessage(ERROR.existingPhone)).not.toBeVisible();
+ await expect(poContacts.contactInfo.btnSave).toBeEnabled();
+ });
+
+ await test.step('validate name is required', async () => {
+ await poContacts.contactInfo.inputName.selectText();
+ await poContacts.contactInfo.inputName.type(' ');
+
+ await expect(poContacts.contactInfo.btnSave).toBeEnabled();
+ await poContacts.contactInfo.btnSave.click();
+ await expect(poContacts.contactInfo.errorMessage(ERROR.nameRequired)).toBeVisible();
+ });
+
+ await test.step('edit name', async () => {
+ await poContacts.contactInfo.inputName.selectText();
+ await poContacts.contactInfo.inputName.type(EDIT_CONTACT.name);
+ });
+
+ await test.step('save new contact ', async () => {
+ await poContacts.contactInfo.btnSave.click();
+ await expect(poContacts.toastSuccess).toBeVisible();
+
+ await poContacts.inputSearch.selectText();
+ await poContacts.inputSearch.type(EDIT_CONTACT.name);
+ await expect(poContacts.findRowByName(EDIT_CONTACT.name)).toBeVisible();
+ });
+ });
+});
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts
new file mode 100644
index 00000000000..0805b8f5e25
--- /dev/null
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-contacts-list.ts
@@ -0,0 +1,34 @@
+import type { Locator, Page } from '@playwright/test';
+
+import { OmnichannelContactInfo } from './omnichannel-info';
+import { OmnichannelManageContact } from './omnichannel-manage-contact';
+
+export class OmnichannelContacts {
+ private readonly page: Page;
+
+ readonly newContact: OmnichannelManageContact;
+
+ readonly contactInfo: OmnichannelContactInfo;
+
+ constructor(page: Page) {
+ this.page = page;
+ this.newContact = new OmnichannelManageContact(page);
+ this.contactInfo = new OmnichannelContactInfo(page);
+ }
+
+ get btnNewContact(): Locator {
+ return this.page.locator('button >> text="New Contact"');
+ }
+
+ get inputSearch(): Locator {
+ return this.page.locator('input[placeholder="Search"]');
+ }
+
+ findRowByName(contactName: string) {
+ return this.page.locator(`td >> text="${contactName}"`);
+ }
+
+ get toastSuccess(): Locator {
+ return this.page.locator('.rcx-toastbar.rcx-toastbar--success');
+ }
+}
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts
new file mode 100644
index 00000000000..a56c2e54f1e
--- /dev/null
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-info.ts
@@ -0,0 +1,13 @@
+import type { Locator } from '@playwright/test';
+
+import { OmnichannelManageContact } from './omnichannel-manage-contact';
+
+export class OmnichannelContactInfo extends OmnichannelManageContact {
+ get btnEdit(): Locator {
+ return this.page.locator('role=button[name="Edit"]');
+ }
+
+ get btnCall(): Locator {
+ return this.page.locator('role=button[name=Call"]');
+ }
+}
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts
new file mode 100644
index 00000000000..cfae81ec608
--- /dev/null
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-manage-contact.ts
@@ -0,0 +1,45 @@
+import type { Locator, Page } from '@playwright/test';
+
+export class OmnichannelManageContact {
+ protected readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ get newContactTitle(): Locator {
+ return this.page.locator('h3 >> text="New Contact"');
+ }
+
+ get editContactTitle(): Locator {
+ return this.page.locator('h3 >> text="Edit Contact Profile"');
+ }
+
+ get inputName(): Locator {
+ return this.page.locator('input[name=name]');
+ }
+
+ get inputEmail(): Locator {
+ return this.page.locator('input[name=email]');
+ }
+
+ get inputPhone(): Locator {
+ return this.page.locator('input[name=phone]');
+ }
+
+ get inputContactManager(): Locator {
+ return this.page.locator('input[name=contactManager]');
+ }
+
+ get btnSave(): Locator {
+ return this.page.locator('button >> text="Save"');
+ }
+
+ get btnCancel(): Locator {
+ return this.page.locator('button >> text="Cancel"');
+ }
+
+ errorMessage(message: string): Locator {
+ return this.page.locator(`.rcx-field__error >> text="${message}"`);
+ }
+}
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts
new file mode 100644
index 00000000000..87a34a66356
--- /dev/null
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-section.ts
@@ -0,0 +1,25 @@
+import type { Locator, Page } from '@playwright/test';
+
+export class OmnichannelSection {
+ private readonly page: Page;
+
+ constructor(page: Page) {
+ this.page = page;
+ }
+
+ get element(): Locator {
+ return this.page.locator('div[data-qa-id="omncSection"]');
+ }
+
+ get btnVoipToggle(): Locator {
+ return this.page.locator('role=button[name="Enable/Disable VoIP"]');
+ }
+
+ get btnDialpad(): Locator {
+ return this.page.locator('role=button[name="Open Dialpad"]');
+ }
+
+ get btnContactCenter(): Locator {
+ return this.page.locator('role=button[name="Contact Center"]');
+ }
+}
diff --git a/apps/meteor/tests/e2e/utils/test.ts b/apps/meteor/tests/e2e/utils/test.ts
index 53da3cfaf61..ebef919905c 100644
--- a/apps/meteor/tests/e2e/utils/test.ts
+++ b/apps/meteor/tests/e2e/utils/test.ts
@@ -13,10 +13,10 @@ export type AnyObj = { [key: string]: any };
export type BaseTest = {
api: {
- get(uri: string, prefix?: string): Promise;
+ get(uri: string, params?: AnyObj, prefix?: string): Promise;
post(uri: string, data: AnyObj, prefix?: string): Promise;
put(uri: string, data: AnyObj, prefix?: string): Promise;
- delete(uri: string, prefix?: string): Promise;
+ delete(uri: string, params?: AnyObj, prefix?: string): Promise;
};
};
@@ -61,8 +61,8 @@ export const test = baseTest.extend({
};
await use({
- get(uri: string, prefix = API_PREFIX) {
- return request.get(BASE_URL + prefix + uri, { headers });
+ get(uri: string, params?: AnyObj, prefix = API_PREFIX) {
+ return request.get(BASE_URL + prefix + uri, { headers, params });
},
post(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.post(BASE_URL + prefix + uri, { headers, data });
@@ -70,8 +70,8 @@ export const test = baseTest.extend({
put(uri: string, data: AnyObj, prefix = API_PREFIX) {
return request.put(BASE_URL + prefix + uri, { headers, data });
},
- delete(uri: string, prefix = API_PREFIX) {
- return request.delete(BASE_URL + prefix + uri, { headers });
+ delete(uri: string, params?: AnyObj, prefix = API_PREFIX) {
+ return request.delete(BASE_URL + prefix + uri, { headers, params });
},
});
},