From 740d9b2c4b43b13fa14c7b9163456b3828e487b1 Mon Sep 17 00:00:00 2001
From: Douglas Fabris <devfabris@gmail.com>
Date: Fri, 10 Nov 2023 17:01:47 -0300
Subject: [PATCH] chore: Refactor Omnichannel EditCustomField UI (#30786)

---
 .../views/omnichannel/additionalForms.tsx     |   4 +-
 .../customFields/CustomFieldsForm.stories.tsx |  27 +--
 .../customFields/CustomFieldsPage.tsx         |  19 +-
 .../customFields/CustomFieldsRoute.tsx        |  22 +-
 .../customFields/CustomFieldsTable.tsx        |  36 +--
 .../customFields/EditCustomFields.tsx         | 226 ++++++++++++++++++
 .../customFields/EditCustomFieldsPage.js      |  87 -------
 .../EditCustomFieldsPageContainer.js          |  35 ---
 .../customFields/EditCustomFieldsWithData.tsx |  29 +++
 .../customFields/NewCustomFieldsForm.js       |  66 -----
 .../customFields/NewCustomFieldsPage.js       |  86 -------
 ...eldButton.tsx => useRemoveCustomField.tsx} |  22 +-
 .../CustomFieldsAdditionalForm.js             |  65 -----
 .../CustomFieldsAdditionalForm.tsx            | 120 ++++++++++
 .../CustomFieldsAdditionalFormContainer.js    |  50 ----
 .../rocketchat-i18n/i18n/en.i18n.json         |   2 +
 .../omnichannel-custom-fields.spec.ts         |   8 +-
 .../page-objects/omnichannel-custom-fields.ts |  18 +-
 packages/rest-typings/src/v1/omnichannel.ts   |   2 +-
 19 files changed, 439 insertions(+), 485 deletions(-)
 create mode 100644 apps/meteor/client/views/omnichannel/customFields/EditCustomFields.tsx
 delete mode 100644 apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js
 delete mode 100644 apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPageContainer.js
 create mode 100644 apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsWithData.tsx
 delete mode 100644 apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsForm.js
 delete mode 100644 apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js
 rename apps/meteor/client/views/omnichannel/customFields/{RemoveCustomFieldButton.tsx => useRemoveCustomField.tsx} (56%)
 delete mode 100644 apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.js
 create mode 100644 apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx
 delete mode 100644 apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalFormContainer.js

diff --git a/apps/meteor/client/views/omnichannel/additionalForms.tsx b/apps/meteor/client/views/omnichannel/additionalForms.tsx
index 0b04097ab154..152245df90be 100644
--- a/apps/meteor/client/views/omnichannel/additionalForms.tsx
+++ b/apps/meteor/client/views/omnichannel/additionalForms.tsx
@@ -1,7 +1,7 @@
 import BusinessHoursMultipleContainer from '../../../ee/client/omnichannel/additionalForms/BusinessHoursMultipleContainer';
 import ContactManager from '../../../ee/client/omnichannel/additionalForms/ContactManager';
 import CurrentChatTags from '../../../ee/client/omnichannel/additionalForms/CurrentChatTags';
-import CustomFieldsAdditionalFormContainer from '../../../ee/client/omnichannel/additionalForms/CustomFieldsAdditionalFormContainer';
+import CustomFieldsAdditionalForm from '../../../ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm';
 import DepartmentBusinessHours from '../../../ee/client/omnichannel/additionalForms/DepartmentBusinessHours';
 import DepartmentForwarding from '../../../ee/client/omnichannel/additionalForms/DepartmentForwarding';
 import EeNumberInput from '../../../ee/client/omnichannel/additionalForms/EeNumberInput';
@@ -13,7 +13,7 @@ import PrioritiesSelect from '../../../ee/client/omnichannel/additionalForms/Pri
 import SlaPoliciesSelect from '../../../ee/client/omnichannel/additionalForms/SlaPoliciesSelect';
 
 export {
-	CustomFieldsAdditionalFormContainer,
+	CustomFieldsAdditionalForm,
 	MaxChatsPerAgentContainer,
 	MaxChatsPerAgentDisplay,
 	EeNumberInput,
diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.stories.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.stories.tsx
index d7dabfe388a8..840581154afd 100644
--- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.stories.tsx
+++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsForm.stories.tsx
@@ -1,13 +1,12 @@
 import { Box } from '@rocket.chat/fuselage';
-import { action } from '@storybook/addon-actions';
 import type { ComponentMeta, ComponentStory } from '@storybook/react';
 import React from 'react';
 
-import NewCustomFieldsForm from './NewCustomFieldsForm';
+import EditCustomFields from './EditCustomFields';
 
 export default {
-	title: 'Omnichannel/NewCustomFieldsForm',
-	component: NewCustomFieldsForm,
+	title: 'Omnichannel/CustomFields',
+	component: EditCustomFields,
 	decorators: [
 		(fn) => (
 			<Box maxWidth='x600' alignSelf='center' w='full' m={24}>
@@ -15,23 +14,7 @@ export default {
 			</Box>
 		),
 	],
-} as ComponentMeta<typeof NewCustomFieldsForm>;
+} as ComponentMeta<typeof EditCustomFields>;
 
-export const Default: ComponentStory<typeof NewCustomFieldsForm> = (args) => <NewCustomFieldsForm {...args} />;
+export const Default: ComponentStory<typeof EditCustomFields> = (args) => <EditCustomFields {...args} />;
 Default.storyName = 'CustomFieldsForm';
-Default.args = {
-	values: {
-		field: '',
-		label: '',
-		scope: 'visitor',
-		visibility: true,
-		regexp: '',
-	},
-	handlers: {
-		handleField: action('handleField'),
-		handleLabel: action('handleLabel'),
-		handleScope: action('handleScope'),
-		handleVisibility: action('handleVisibility'),
-		handleRegexp: action('handleRegexp'),
-	},
-};
diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx
index 6ca2fea326c5..4ee813159221 100644
--- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx
+++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx
@@ -1,30 +1,33 @@
 import { Button } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useRoute, useTranslation } from '@rocket.chat/ui-contexts';
-import type { MutableRefObject } from 'react';
+import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts';
 import React from 'react';
 
 import Page from '../../../components/Page';
 import CustomFieldsTable from './CustomFieldsTable';
+import EditCustomFields from './EditCustomFields';
+import EditCustomFieldsWithData from './EditCustomFieldsWithData';
 
-const CustomFieldsPage = ({ reload }: { reload: MutableRefObject<() => void> }) => {
+const CustomFieldsPage = () => {
 	const t = useTranslation();
-	const router = useRoute('omnichannel-customfields');
+	const router = useRouter();
 
-	const onAddNew = useMutableCallback(() => router.push({ context: 'new' }));
+	const context = useRouteParameter('context');
+	const id = useRouteParameter('id');
 
 	return (
 		<Page flexDirection='row'>
 			<Page>
 				<Page.Header title={t('Custom_Fields')}>
-					<Button data-qa-id='CustomFieldPageBtnNew' onClick={onAddNew}>
+					<Button data-qa-id='CustomFieldPageBtnNew' onClick={() => router.navigate('/omnichannel/customfields/new')}>
 						{t('Create_custom_field')}
 					</Button>
 				</Page.Header>
 				<Page.Content>
-					<CustomFieldsTable reload={reload} />
+					<CustomFieldsTable />
 				</Page.Content>
 			</Page>
+			{context === 'edit' && id && <EditCustomFieldsWithData customFieldId={id} />}
+			{context === 'new' && <EditCustomFields />}
 		</Page>
 	);
 };
diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.tsx
index 4297a26fc924..2ffc7580e3fa 100644
--- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.tsx
+++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsRoute.tsx
@@ -1,33 +1,17 @@
-import { useRouteParameter, usePermission } from '@rocket.chat/ui-contexts';
-import React, { useRef, useCallback } from 'react';
+import { usePermission } from '@rocket.chat/ui-contexts';
+import React from 'react';
 
 import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
 import CustomFieldsPage from './CustomFieldsPage';
-import EditCustomFieldsPage from './EditCustomFieldsPageContainer';
-import NewCustomFieldsPage from './NewCustomFieldsPage';
 
 const CustomFieldsRoute = () => {
-	const reload = useRef(() => null);
 	const canViewCustomFields = usePermission('view-livechat-customfields');
-	const context = useRouteParameter('context');
-
-	const handleReload = useCallback(() => {
-		reload.current();
-	}, [reload]);
 
 	if (!canViewCustomFields) {
 		return <NotAuthorizedPage />;
 	}
 
-	if (context === 'new') {
-		return <NewCustomFieldsPage reload={handleReload} />;
-	}
-
-	if (context === 'edit') {
-		return <EditCustomFieldsPage reload={handleReload} />;
-	}
-
-	return <CustomFieldsPage reload={reload} />;
+	return <CustomFieldsPage />;
 };
 
 export default CustomFieldsRoute;
diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx
index f9872c710a5e..3298139319b2 100644
--- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx
+++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx
@@ -1,9 +1,8 @@
-import { Pagination } from '@rocket.chat/fuselage';
+import { IconButton, Pagination } from '@rocket.chat/fuselage';
 import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks';
 import { useTranslation, useEndpoint, useRouter } from '@rocket.chat/ui-contexts';
 import { useQuery, hashQueryKey } from '@tanstack/react-query';
-import type { MutableRefObject } from 'react';
-import React, { useMemo, useState, useEffect } from 'react';
+import React, { useMemo, useState } from 'react';
 
 import FilterByText from '../../../components/FilterByText';
 import GenericNoResults from '../../../components/GenericNoResults';
@@ -18,9 +17,9 @@ import {
 } from '../../../components/GenericTable';
 import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
 import { useSort } from '../../../components/GenericTable/hooks/useSort';
-import RemoveCustomFieldButton from './RemoveCustomFieldButton';
+import { useRemoveCustomField } from './useRemoveCustomField';
 
-const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => {
+const CustomFieldsTable = () => {
 	const t = useTranslation();
 	const router = useRouter();
 	const [filter, setFilter] = useState('');
@@ -32,6 +31,8 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> })
 	const handleAddNew = useMutableCallback(() => router.navigate('/omnichannel/customfields/new'));
 	const onRowClick = useMutableCallback((id) => () => router.navigate(`/omnichannel/customfields/edit/${id}`));
 
+	const handleDelete = useRemoveCustomField();
+
 	const query = useMemo(
 		() => ({
 			text: debouncedFilter,
@@ -43,18 +44,11 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> })
 	);
 
 	const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields');
-	const { data, isSuccess, isLoading, refetch } = useQuery(['livechat-customFields', query, debouncedFilter], async () =>
-		getCustomFields(query),
-	);
+	const { data, isSuccess, isLoading } = useQuery(['livechat-customFields', query, debouncedFilter], async () => getCustomFields(query));
 
 	const [defaultQuery] = useState(hashQueryKey([query]));
 	const queryHasChanged = defaultQuery !== hashQueryKey([query]);
 
-	useEffect(() => {
-		reload.current = refetch;
-	}, [reload, refetch]);
-	reload.current = refetch;
-
 	const headers = (
 		<>
 			<GenericTableHeaderCell key='field' direction={sortDirection} active={sortBy === '_id'} onClick={setSort} sort='_id'>
@@ -75,9 +69,7 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> })
 			>
 				{t('Visibility')}
 			</GenericTableHeaderCell>
-			<GenericTableHeaderCell key='remove' w='x60'>
-				{t('Remove')}
-			</GenericTableHeaderCell>
+			<GenericTableHeaderCell key='remove' w='x60' />
 		</>
 	);
 
@@ -116,7 +108,17 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> })
 									<GenericTableCell withTruncatedText>{label}</GenericTableCell>
 									<GenericTableCell withTruncatedText>{scope === 'visitor' ? t('Visitor') : t('Room')}</GenericTableCell>
 									<GenericTableCell withTruncatedText>{visibility === 'visible' ? t('Visible') : t('Hidden')}</GenericTableCell>
-									<RemoveCustomFieldButton _id={_id} reload={refetch} />
+									<GenericTableCell withTruncatedText>
+										<IconButton
+											icon='trash'
+											small
+											title={t('Remove')}
+											onClick={(e) => {
+												e.stopPropagation();
+												handleDelete(_id);
+											}}
+										/>
+									</GenericTableCell>
 								</GenericTableRow>
 							))}
 						</GenericTableBody>
diff --git a/apps/meteor/client/views/omnichannel/customFields/EditCustomFields.tsx b/apps/meteor/client/views/omnichannel/customFields/EditCustomFields.tsx
new file mode 100644
index 000000000000..5a96076bd1b3
--- /dev/null
+++ b/apps/meteor/client/views/omnichannel/customFields/EditCustomFields.tsx
@@ -0,0 +1,226 @@
+import type { ILivechatCustomField, Serialized } from '@rocket.chat/core-typings';
+import type { SelectOption } from '@rocket.chat/fuselage';
+import {
+	FieldError,
+	Box,
+	Button,
+	ButtonGroup,
+	Field,
+	FieldGroup,
+	FieldLabel,
+	FieldRow,
+	Select,
+	TextInput,
+	ToggleSwitch,
+} from '@rocket.chat/fuselage';
+import { useMutableCallback, useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useToastMessageDispatch, useMethod, useTranslation, useRouter } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
+import React, { useMemo } from 'react';
+import { FormProvider, useForm, Controller } from 'react-hook-form';
+
+import {
+	Contextualbar,
+	ContextualbarTitle,
+	ContextualbarHeader,
+	ContextualbarClose,
+	ContextualbarFooter,
+	ContextualbarScrollableContent,
+} from '../../../components/Contextualbar';
+import { CustomFieldsAdditionalForm } from '../additionalForms';
+import { useRemoveCustomField } from './useRemoveCustomField';
+
+const getInitialValues = (customFieldData: Serialized<ILivechatCustomField> | undefined) => ({
+	field: customFieldData?._id || '',
+	label: customFieldData?.label || '',
+	scope: customFieldData?.scope || 'visitor',
+	visibility: customFieldData?.visibility === 'visible',
+	searchable: !!customFieldData?.searchable,
+	regexp: customFieldData?.regexp || '',
+	// additional props
+	type: customFieldData?.type || 'input',
+	required: !!customFieldData?.required,
+	defaultValue: customFieldData?.defaultValue || '',
+	options: customFieldData?.options || '',
+	public: !!customFieldData?.public,
+});
+
+const EditCustomFields = ({ customFieldData }: { customFieldData?: Serialized<ILivechatCustomField> }) => {
+	const t = useTranslation();
+	const router = useRouter();
+	const queryClient = useQueryClient();
+	const dispatchToastMessage = useToastMessageDispatch();
+
+	const handleDelete = useRemoveCustomField();
+
+	const methods = useForm({ mode: 'onBlur', values: getInitialValues(customFieldData) });
+	const {
+		control,
+		handleSubmit,
+		formState: { isDirty, errors },
+	} = methods;
+
+	const saveCustomField = useMethod('livechat:saveCustomField');
+
+	const handleSave = useMutableCallback(async ({ visibility, ...data }) => {
+		try {
+			await saveCustomField(customFieldData?._id as unknown as string, {
+				visibility: visibility ? 'visible' : 'hidden',
+				...data,
+			});
+
+			dispatchToastMessage({ type: 'success', message: t('Saved') });
+			queryClient.invalidateQueries(['livechat-customFields']);
+			router.navigate('/omnichannel/customfields');
+		} catch (error) {
+			dispatchToastMessage({ type: 'error', message: error });
+		}
+	});
+
+	const scopeOptions: SelectOption[] = useMemo(
+		() => [
+			['visitor', t('Visitor')],
+			['room', t('Room')],
+		],
+		[t],
+	);
+
+	const formId = useUniqueId();
+	const fieldField = useUniqueId();
+	const labelField = useUniqueId();
+	const scopeField = useUniqueId();
+	const visibilityField = useUniqueId();
+	const searchableField = useUniqueId();
+	const regexpField = useUniqueId();
+
+	return (
+		<Contextualbar>
+			<ContextualbarHeader>
+				<ContextualbarTitle>{customFieldData?._id ? t('Edit_Custom_Field') : t('New_Custom_Field')}</ContextualbarTitle>
+				<ContextualbarClose onClick={() => router.navigate('/omnichannel/customfields')} />
+			</ContextualbarHeader>
+			<ContextualbarScrollableContent>
+				<FormProvider {...methods}>
+					<form id={formId} onSubmit={handleSubmit(handleSave)}>
+						<FieldGroup>
+							<Field>
+								<FieldLabel htmlFor={fieldField} required>
+									{t('Field')}
+								</FieldLabel>
+								<FieldRow>
+									<Controller
+										name='field'
+										control={control}
+										rules={{
+											required: t('The_field_is_required', t('Field')),
+											validate: (value) => (!/^[0-9a-zA-Z-_]+$/.test(value) ? t('error-invalid-custom-field-name') : undefined),
+										}}
+										render={({ field }) => (
+											<TextInput
+												id={fieldField}
+												{...field}
+												readOnly={Boolean(customFieldData?._id)}
+												aria-required={true}
+												aria-invalid={Boolean(errors.field)}
+												aria-describedby={`${fieldField}-error`}
+											/>
+										)}
+									/>
+								</FieldRow>
+								{errors?.field && (
+									<FieldError aria-live='assertive' id={`${fieldField}-error`}>
+										{errors.field.message}
+									</FieldError>
+								)}
+							</Field>
+							<Field>
+								<FieldLabel htmlFor={labelField} required>
+									{t('Label')}
+								</FieldLabel>
+								<FieldRow>
+									<Controller
+										name='label'
+										control={control}
+										rules={{ required: t('The_field_is_required', t('Label')) }}
+										render={({ field }) => (
+											<TextInput
+												id={labelField}
+												{...field}
+												aria-required={true}
+												aria-invalid={Boolean(errors.label)}
+												aria-describedby={`${labelField}-error`}
+											/>
+										)}
+									/>
+								</FieldRow>
+								{errors?.label && (
+									<FieldError aria-live='assertive' id={`${labelField}-error`}>
+										{errors.label.message}
+									</FieldError>
+								)}
+							</Field>
+							<Field>
+								<FieldLabel htmlFor={scopeField}>{t('Scope')}</FieldLabel>
+								<FieldRow>
+									<Controller
+										name='scope'
+										control={control}
+										render={({ field }) => <Select id={scopeField} {...field} options={scopeOptions} />}
+									/>
+								</FieldRow>
+							</Field>
+							<Field>
+								<Box display='flex' flexDirection='row'>
+									<FieldLabel htmlFor={visibilityField}>{t('Visible')}</FieldLabel>
+									<FieldRow>
+										<Controller
+											name='visibility'
+											control={control}
+											render={({ field: { value, ...field } }) => <ToggleSwitch id={visibilityField} {...field} checked={value} />}
+										/>
+									</FieldRow>
+								</Box>
+							</Field>
+							<Field>
+								<Box display='flex' flexDirection='row'>
+									<FieldLabel htmlFor={searchableField}>{t('Searchable')}</FieldLabel>
+									<FieldRow>
+										<Controller
+											name='searchable'
+											control={control}
+											render={({ field: { value, ...field } }) => <ToggleSwitch id={searchableField} {...field} checked={value} />}
+										/>
+									</FieldRow>
+								</Box>
+							</Field>
+							<Field>
+								<FieldLabel htmlFor={regexpField}>{t('Validation')}</FieldLabel>
+								<FieldRow>
+									<Controller name='regexp' control={control} render={({ field }) => <TextInput id={regexpField} {...field} />} />
+								</FieldRow>
+							</Field>
+							{CustomFieldsAdditionalForm && <CustomFieldsAdditionalForm />}
+						</FieldGroup>
+					</form>
+				</FormProvider>
+			</ContextualbarScrollableContent>
+			<ContextualbarFooter>
+				<ButtonGroup stretch>
+					<Button onClick={() => router.navigate('/omnichannel/customfields')}>{t('Cancel')}</Button>
+					<Button form={formId} data-qa-id='BtnSaveEditCustomFieldsPage' primary type='submit' disabled={!isDirty}>
+						{t('Save')}
+					</Button>
+				</ButtonGroup>
+				{customFieldData?._id && (
+					<ButtonGroup stretch mbs={8}>
+						<Button icon='trash' danger onClick={() => handleDelete(customFieldData._id)}>
+							{t('Delete')}
+						</Button>
+					</ButtonGroup>
+				)}
+			</ContextualbarFooter>
+		</Contextualbar>
+	);
+};
+
+export default EditCustomFields;
diff --git a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js
deleted file mode 100644
index c00fe4dce672..000000000000
--- a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPage.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Box, Button, ButtonGroup, FieldGroup } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useCallback, useState } from 'react';
-
-import Page from '../../../components/Page';
-import { useForm } from '../../../hooks/useForm';
-import { CustomFieldsAdditionalFormContainer } from '../additionalForms';
-import NewCustomFieldsForm from './NewCustomFieldsForm';
-
-const getInitialValues = (cf) => ({
-	id: cf._id,
-	field: cf._id,
-	label: cf.label,
-	scope: cf.scope,
-	visibility: cf.visibility === 'visible',
-	searchable: !!cf.searchable,
-	regexp: cf.regexp,
-});
-
-const EditCustomFieldsPage = ({ customField, id, reload }) => {
-	const t = useTranslation();
-	const dispatchToastMessage = useToastMessageDispatch();
-
-	const [additionalValues, setAdditionalValues] = useState({});
-
-	const router = useRoute('omnichannel-customfields');
-
-	const handleReturn = useCallback(() => {
-		router.push({});
-	}, [router]);
-
-	const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(customField));
-
-	const save = useMethod('livechat:saveCustomField');
-
-	const { hasError, data: additionalData, hasUnsavedChanges: additionalFormChanged } = additionalValues;
-
-	const { label, field } = values;
-
-	const canSave = !hasError && label && field && (additionalFormChanged || hasUnsavedChanges);
-
-	const handleSave = useMutableCallback(async () => {
-		try {
-			await save(id, {
-				...additionalData,
-				...values,
-				visibility: values.visibility ? 'visible' : 'hidden',
-			});
-
-			dispatchToastMessage({ type: 'success', message: t('Saved') });
-			reload();
-			router.push({});
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const handleAdditionalForm = useMutableCallback((val) => {
-		setAdditionalValues({ ...additionalValues, ...val });
-	});
-
-	return (
-		<Page>
-			<Page.Header title={t('Edit_Custom_Field')}>
-				<ButtonGroup align='end'>
-					<Button icon='back' onClick={handleReturn}>
-						{t('Back')}
-					</Button>
-					<Button data-qa-id='BtnSaveEditCustomFieldsPage' primary onClick={handleSave} disabled={!canSave}>
-						{t('Save')}
-					</Button>
-				</ButtonGroup>
-			</Page.Header>
-			<Page.ScrollableContentWithShadow>
-				<Box maxWidth='x600' w='full' alignSelf='center'>
-					<FieldGroup>
-						<NewCustomFieldsForm values={values} handlers={handlers} />
-						<CustomFieldsAdditionalFormContainer onChange={handleAdditionalForm} state={values} data={customField} />
-					</FieldGroup>
-				</Box>
-			</Page.ScrollableContentWithShadow>
-		</Page>
-	);
-};
-
-export default EditCustomFieldsPage;
diff --git a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPageContainer.js b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPageContainer.js
deleted file mode 100644
index be3f83c55da4..000000000000
--- a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsPageContainer.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Callout } from '@rocket.chat/fuselage';
-import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts';
-import React from 'react';
-
-import Page from '../../../components/Page';
-import PageSkeleton from '../../../components/PageSkeleton';
-import { AsyncStatePhase } from '../../../hooks/useAsyncState';
-import { useEndpointData } from '../../../hooks/useEndpointData';
-import EditCustomFieldsPage from './EditCustomFieldsPage';
-
-const EditCustomFieldsPageContainer = ({ reload }) => {
-	const t = useTranslation();
-	const id = useRouteParameter('id');
-
-	const { value: data, phase: state, error } = useEndpointData('/v1/livechat/custom-fields/:_id', { keys: { _id: id } });
-
-	if (state === AsyncStatePhase.LOADING) {
-		return <PageSkeleton />;
-	}
-
-	if (!data || !data.success || !data.customField || error) {
-		return (
-			<Page>
-				<Page.Header title={t('Edit_Custom_Field')} />
-				<Page.ScrollableContentWithShadow>
-					<Callout type='danger'>{t('Error')}</Callout>
-				</Page.ScrollableContentWithShadow>
-			</Page>
-		);
-	}
-
-	return <EditCustomFieldsPage customField={data.customField} id={id} reload={reload} />;
-};
-
-export default EditCustomFieldsPageContainer;
diff --git a/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsWithData.tsx b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsWithData.tsx
new file mode 100644
index 000000000000..cf5a1b918e95
--- /dev/null
+++ b/apps/meteor/client/views/omnichannel/customFields/EditCustomFieldsWithData.tsx
@@ -0,0 +1,29 @@
+import type { ILivechatCustomField } from '@rocket.chat/core-typings';
+import { Callout } from '@rocket.chat/fuselage';
+import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
+import { useQuery } from '@tanstack/react-query';
+import React from 'react';
+
+import PageSkeleton from '../../../components/PageSkeleton';
+import EditCustomFields from './EditCustomFields';
+
+const EditCustomFieldsWithData = ({ customFieldId }: { customFieldId: ILivechatCustomField['_id'] }) => {
+	const t = useTranslation();
+
+	const getCustomFieldById = useEndpoint('GET', '/v1/livechat/custom-fields/:_id', { _id: customFieldId });
+	const { data, isLoading, isError } = useQuery(['livechat-getCustomFieldsById', customFieldId], async () => getCustomFieldById(), {
+		refetchOnWindowFocus: false,
+	});
+
+	if (isLoading) {
+		return <PageSkeleton />;
+	}
+
+	if (isError) {
+		return <Callout type='danger'>{t('Error')}</Callout>;
+	}
+
+	return <EditCustomFields customFieldData={data?.customField} />;
+};
+
+export default EditCustomFieldsWithData;
diff --git a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsForm.js b/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsForm.js
deleted file mode 100644
index 5ef97a6e8e3d..000000000000
--- a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsForm.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { Box, Field, TextInput, ToggleSwitch, Select } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useMemo } from 'react';
-
-const NewCustomFieldsForm = ({ values = {}, handlers = {}, className }) => {
-	const t = useTranslation();
-
-	const { id, field, label, scope, visibility, searchable, regexp } = values;
-
-	const { handleField, handleLabel, handleScope, handleVisibility, handleSearchable, handleRegexp } = handlers;
-
-	const scopeOptions = useMemo(
-		() => [
-			['visitor', t('Visitor')],
-			['room', t('Room')],
-		],
-		[t],
-	);
-
-	return (
-		<>
-			<Field className={className}>
-				<Field.Label>{t('Field')}*</Field.Label>
-				<Field.Row>
-					<TextInput disabled={id} value={field} onChange={handleField} placeholder={t('Field')} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Label')}*</Field.Label>
-				<Field.Row>
-					<TextInput value={label} onChange={handleLabel} placeholder={t('Label')} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Scope')}</Field.Label>
-				<Field.Row>
-					<Select options={scopeOptions} value={scope} onChange={handleScope} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Box display='flex' flexDirection='row'>
-					<Field.Label htmlFor='visible'>{t('Visible')}</Field.Label>
-					<Field.Row>
-						<ToggleSwitch id='visible' checked={visibility} onChange={handleVisibility} />
-					</Field.Row>
-				</Box>
-			</Field>
-			<Field className={className}>
-				<Box display='flex' flexDirection='row'>
-					<Field.Label htmlFor='searchable'>{t('Searchable')}</Field.Label>
-					<Field.Row>
-						<ToggleSwitch id='searchable' checked={searchable} onChange={handleSearchable} />
-					</Field.Row>
-				</Box>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Validation')}</Field.Label>
-				<Field.Row>
-					<TextInput value={regexp} onChange={handleRegexp} placeholder={t('Validation')} />
-				</Field.Row>
-			</Field>
-		</>
-	);
-};
-
-export default NewCustomFieldsForm;
diff --git a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js b/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js
deleted file mode 100644
index e5ba5a892808..000000000000
--- a/apps/meteor/client/views/omnichannel/customFields/NewCustomFieldsPage.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { Box, Button, FieldGroup, ButtonGroup } from '@rocket.chat/fuselage';
-import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
-import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useCallback, useState } from 'react';
-
-import Page from '../../../components/Page';
-import { useForm } from '../../../hooks/useForm';
-import { CustomFieldsAdditionalFormContainer } from '../additionalForms';
-import NewCustomFieldsForm from './NewCustomFieldsForm';
-
-const initialValues = {
-	field: '',
-	label: '',
-	scope: 'visitor',
-	visibility: true,
-	regexp: '',
-	searchable: true,
-};
-
-const NewCustomFieldsPage = ({ reload }) => {
-	const t = useTranslation();
-	const dispatchToastMessage = useToastMessageDispatch();
-
-	const [additionalValues, setAdditionalValues] = useState({});
-
-	const router = useRoute('omnichannel-customfields');
-
-	const handleReturn = useCallback(() => {
-		router.push({});
-	}, [router]);
-
-	const { values, handlers, hasUnsavedChanges } = useForm(initialValues);
-
-	const save = useMethod('livechat:saveCustomField');
-
-	const { hasError, data: additionalData, hasUnsavedChanges: additionalFormChanged } = additionalValues;
-
-	const { label, field } = values;
-
-	const canSave = !hasError && label && field && (additionalFormChanged || hasUnsavedChanges);
-
-	const handleSave = useMutableCallback(async () => {
-		try {
-			await save(undefined, {
-				...values,
-				visibility: values.visibility ? 'visible' : 'hidden',
-				...additionalData,
-			});
-
-			dispatchToastMessage({ type: 'success', message: t('Saved') });
-			reload();
-			router.push({});
-		} catch (error) {
-			dispatchToastMessage({ type: 'error', message: error });
-		}
-	});
-
-	const handleAdditionalForm = useMutableCallback((val) => {
-		setAdditionalValues({ ...additionalValues, ...val });
-	});
-
-	return (
-		<Page>
-			<Page.Header title={t('New_Custom_Field')}>
-				<ButtonGroup>
-					<Button icon='back' onClick={handleReturn}>
-						{t('Back')}
-					</Button>
-					<Button data-qa-id='NewCustomFieldsPageButtonSave' primary onClick={handleSave} disabled={!canSave}>
-						{t('Save')}
-					</Button>
-				</ButtonGroup>
-			</Page.Header>
-			<Page.ScrollableContentWithShadow>
-				<Box maxWidth='x600' w='full' alignSelf='center'>
-					<FieldGroup>
-						<NewCustomFieldsForm values={values} handlers={handlers} />
-						<CustomFieldsAdditionalFormContainer onChange={handleAdditionalForm} state={values} />
-					</FieldGroup>
-				</Box>
-			</Page.ScrollableContentWithShadow>
-		</Page>
-	);
-};
-
-export default NewCustomFieldsPage;
diff --git a/apps/meteor/client/views/omnichannel/customFields/RemoveCustomFieldButton.tsx b/apps/meteor/client/views/omnichannel/customFields/useRemoveCustomField.tsx
similarity index 56%
rename from apps/meteor/client/views/omnichannel/customFields/RemoveCustomFieldButton.tsx
rename to apps/meteor/client/views/omnichannel/customFields/useRemoveCustomField.tsx
index 9f21fdd5d5ee..f4bbeb62a770 100644
--- a/apps/meteor/client/views/omnichannel/customFields/RemoveCustomFieldButton.tsx
+++ b/apps/meteor/client/views/omnichannel/customFields/useRemoveCustomField.tsx
@@ -1,25 +1,23 @@
-import type { ILivechatCustomField } from '@rocket.chat/core-typings';
-import { IconButton } from '@rocket.chat/fuselage';
 import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
 import { useSetModal, useToastMessageDispatch, useMethod, useTranslation } from '@rocket.chat/ui-contexts';
+import { useQueryClient } from '@tanstack/react-query';
 import React from 'react';
 
 import GenericModal from '../../../components/GenericModal';
-import { GenericTableCell } from '../../../components/GenericTable';
 
-const RemoveCustomFieldButton = ({ _id, reload }: { _id: ILivechatCustomField['_id']; reload: () => void }) => {
+export const useRemoveCustomField = () => {
 	const t = useTranslation();
 	const setModal = useSetModal();
 	const dispatchToastMessage = useToastMessageDispatch();
 	const removeCustomField = useMethod('livechat:removeCustomField');
+	const queryClient = useQueryClient();
 
-	const handleDelete = useMutableCallback((e) => {
-		e.stopPropagation();
+	const handleDelete = useMutableCallback((id) => {
 		const onDeleteAgent = async () => {
 			try {
-				await removeCustomField(_id);
+				await removeCustomField(id);
 				dispatchToastMessage({ type: 'success', message: t('Custom_Field_Removed') });
-				reload();
+				queryClient.invalidateQueries(['livechat-customFields']);
 			} catch (error) {
 				dispatchToastMessage({ type: 'error', message: error });
 			} finally {
@@ -30,11 +28,5 @@ const RemoveCustomFieldButton = ({ _id, reload }: { _id: ILivechatCustomField['_
 		setModal(<GenericModal variant='danger' onConfirm={onDeleteAgent} onCancel={() => setModal()} confirmText={t('Delete')} />);
 	});
 
-	return (
-		<GenericTableCell fontScale='p2' color='hint' withTruncatedText>
-			<IconButton icon='trash' small title={t('Remove')} onClick={handleDelete} />
-		</GenericTableCell>
-	);
+	return handleDelete;
 };
-
-export default RemoveCustomFieldButton;
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.js b/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.js
deleted file mode 100644
index 4650d08135c8..000000000000
--- a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.js
+++ /dev/null
@@ -1,65 +0,0 @@
-import { Box, Field, TextInput, ToggleSwitch, Select } from '@rocket.chat/fuselage';
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useMemo } from 'react';
-
-const CustomFieldsAdditionalForm = ({ values = {}, handlers = {}, state, className, errors }) => {
-	const t = useTranslation();
-
-	const { type, required, defaultValue, options, public: isPublic } = values;
-
-	const { handleType, handleRequired, handleDefaultValue, handleOptions, handlePublic } = handlers;
-
-	const { optionsError } = errors;
-
-	const typeOptions = useMemo(
-		() => [
-			['input', t('Input')],
-			['select', t('Select')],
-		],
-		[t],
-	);
-
-	return (
-		<>
-			<Field className={className}>
-				<Box display='flex' flexDirection='row'>
-					<Field.Label htmlFor='required'>{t('Required')}</Field.Label>
-					<Field.Row>
-						<ToggleSwitch id='required' checked={required} onChange={handleRequired} />
-					</Field.Row>
-				</Box>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Type')}</Field.Label>
-				<Field.Row>
-					<Select options={typeOptions} value={type} onChange={handleType} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Default_value')}</Field.Label>
-				<Field.Row>
-					<TextInput value={defaultValue} onChange={handleDefaultValue} placeholder={t('Default_value')} />
-				</Field.Row>
-			</Field>
-			<Field className={className}>
-				<Field.Label>{t('Options')}</Field.Label>
-				<Field.Row>
-					<TextInput value={options} onChange={handleOptions} error={optionsError} disabled={type === 'input'} placeholder={t('Options')} />
-				</Field.Row>
-				<Field.Hint>{t('Livechat_custom_fields_options_placeholder')}</Field.Hint>
-				{optionsError && <Field.Error>{optionsError}</Field.Error>}
-			</Field>
-			<Field className={className}>
-				<Box display='flex' flexDirection='row'>
-					<Field.Label htmlFor='public'>{t('Public')}</Field.Label>
-					<Field.Row>
-						<ToggleSwitch disabled={!state.visibility} id='public' checked={isPublic} onChange={handlePublic} />
-					</Field.Row>
-				</Box>
-				<Field.Hint>{t('Livechat_custom_fields_public_description')}</Field.Hint>
-			</Field>
-		</>
-	);
-};
-
-export default CustomFieldsAdditionalForm;
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx b/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx
new file mode 100644
index 000000000000..09796bcf29bb
--- /dev/null
+++ b/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalForm.tsx
@@ -0,0 +1,120 @@
+import type { SelectOption } from '@rocket.chat/fuselage';
+import { Field, FieldLabel, FieldRow, FieldError, FieldHint, ToggleSwitch, TextInput, Box, Select } from '@rocket.chat/fuselage';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import type { ComponentProps } from 'react';
+import React, { useMemo } from 'react';
+import { useFormContext, Controller } from 'react-hook-form';
+
+import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
+
+const checkIsOptionsValid = (value: string) => {
+	if (!value || value.trim() === '') {
+		return false;
+	}
+
+	return value.split(',').every((v) => /^[a-zA-Z0-9-_ ]+$/.test(v));
+};
+
+const CustomFieldsAdditionalForm = ({ className }: { className?: ComponentProps<typeof Field>['className'] }) => {
+	const t = useTranslation();
+	const {
+		control,
+		watch,
+		formState: { errors },
+	} = useFormContext();
+	const hasLicense = useHasLicenseModule('livechat-enterprise');
+
+	const { visibility, type } = watch();
+
+	const typeOptions: SelectOption[] = useMemo(
+		() => [
+			['input', t('Input')],
+			['select', t('Select')],
+		],
+		[t],
+	);
+
+	const requiredField = useUniqueId();
+	const typeField = useUniqueId();
+	const defaultValueField = useUniqueId();
+	const optionsField = useUniqueId();
+	const publicField = useUniqueId();
+
+	if (!hasLicense) {
+		return null;
+	}
+
+	return (
+		<>
+			<Field className={className}>
+				<Box display='flex' flexDirection='row'>
+					<FieldLabel htmlFor={requiredField}>{t('Required')}</FieldLabel>
+					<FieldRow>
+						<Controller
+							name='required'
+							control={control}
+							render={({ field: { value, ...field } }) => <ToggleSwitch id={requiredField} {...field} checked={value} />}
+						/>
+					</FieldRow>
+				</Box>
+			</Field>
+			<Field className={className}>
+				<FieldLabel htmlFor={typeField}>{t('Type')}</FieldLabel>
+				<FieldRow>
+					<Controller name='type' control={control} render={({ field }) => <Select id={typeField} options={typeOptions} {...field} />} />
+				</FieldRow>
+			</Field>
+			<Field className={className}>
+				<FieldLabel htmlFor={defaultValueField}>{t('Default_value')}</FieldLabel>
+				<FieldRow>
+					<Controller name='defaultValue' control={control} render={({ field }) => <TextInput id={defaultValueField} {...field} />} />
+				</FieldRow>
+			</Field>
+			<Field className={className}>
+				<FieldLabel htmlFor={optionsField}>{t('Options')}</FieldLabel>
+				<FieldRow>
+					<Controller
+						name='options'
+						control={control}
+						rules={{
+							validate: (optionsValue) => (type === 'select' && !checkIsOptionsValid(optionsValue) ? t('error-invalid-value') : undefined),
+						}}
+						render={({ field }) => (
+							<TextInput
+								id={optionsField}
+								{...field}
+								disabled={type === 'input'}
+								aria-invalid={Boolean(errors?.options)}
+								aria-describedby={`${optionsField}-hint ${optionsField}-error`}
+							/>
+						)}
+					/>
+				</FieldRow>
+				<FieldHint id={`${optionsField}-hint`}>{t('Livechat_custom_fields_options_placeholder')}</FieldHint>
+				{errors.options && (
+					<FieldError aria-live='assertive' id={`${optionsField}-error`}>
+						{errors.options.message}
+					</FieldError>
+				)}
+			</Field>
+			<Field className={className}>
+				<Box display='flex' flexDirection='row'>
+					<FieldLabel htmlFor={publicField}>{t('Public')}</FieldLabel>
+					<FieldRow>
+						<Controller
+							name='public'
+							control={control}
+							render={({ field: { value, ...field } }) => (
+								<ToggleSwitch id={publicField} {...field} disabled={!visibility} checked={value} aria-describedby={`${publicField}-hint`} />
+							)}
+						/>
+					</FieldRow>
+				</Box>
+				<FieldHint id={`${publicField}-hint`}>{t('Livechat_custom_fields_public_description')}</FieldHint>
+			</Field>
+		</>
+	);
+};
+
+export default CustomFieldsAdditionalForm;
diff --git a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalFormContainer.js b/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalFormContainer.js
deleted file mode 100644
index e5c8a37b9c05..000000000000
--- a/apps/meteor/ee/client/omnichannel/additionalForms/CustomFieldsAdditionalFormContainer.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import { useTranslation } from '@rocket.chat/ui-contexts';
-import React, { useMemo, useEffect } from 'react';
-
-import { useForm } from '../../../../client/hooks/useForm';
-import { useHasLicenseModule } from '../../hooks/useHasLicenseModule';
-import CustomFieldsAdditionalForm from './CustomFieldsAdditionalForm';
-
-const getInitialValues = (data) => ({
-	type: data.type || 'input',
-	required: !!data.required,
-	defaultValue: data.defaultValue ?? '',
-	options: data.options || '',
-	public: !!data.public,
-});
-
-const checkInvalidOptions = (value) => {
-	if (!value || value.trim() === '') {
-		return false;
-	}
-
-	return value.split(',').every((v) => /^[a-zA-Z0-9-_ ]+$/.test(v));
-};
-
-const CustomFieldsAdditionalFormContainer = ({ data = {}, state, onChange, className }) => {
-	const t = useTranslation();
-	const hasLicense = useHasLicenseModule('livechat-enterprise');
-
-	const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data));
-
-	const errors = useMemo(
-		() => ({
-			optionsError: checkInvalidOptions(values.options) ? t('error-invalid-value') : undefined,
-		}),
-		[t, values.options],
-	);
-
-	const hasError = useMemo(() => !!Object.values(errors).filter(Boolean).length, [errors]);
-
-	useEffect(() => {
-		onChange({ data: values, hasError, hasUnsavedChanges });
-	}, [hasError, hasUnsavedChanges, onChange, values]);
-
-	if (!hasLicense) {
-		return null;
-	}
-
-	return <CustomFieldsAdditionalForm values={values} handlers={handlers} state={state} className={className} errors={errors} />;
-};
-
-export default CustomFieldsAdditionalFormContainer;
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 18e98bc2c800..1b84e684b02d 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -4620,6 +4620,7 @@
   "See_full_profile": "See full profile",
   "See_history": "See history",
   "See_on_Engagement_Dashboard": "See on Engagement Dashboard",
+  "Select": "Select",
   "Select_a_department": "Select a department",
   "Select_a_room": "Select a room",
   "Select_a_user": "Select a user",
@@ -5160,6 +5161,7 @@
   "This_is_a_deprecated_feature_alert": "This is a deprecated feature. It may not work as expected and will not get new updates.",
   "Zapier_integration_has_been_deprecated": "The Zapier integration has been deprecated, may not work as expected and will not receive updates",
   "Install_Zapier_from_marketplace": "Install the Zapier app from Marketplace to avoid disruptions",
+  "Input": "Input",
   "This_is_a_push_test_messsage": "This is a push test message",
   "This_message_was_rejected_by__peer__peer": "This message was rejected by <em>{{peer}}</em> peer.",
   "This_monitor_was_already_selected": "This monitor was already selected",
diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts
index dd5c93609f6e..472e9bbcc807 100644
--- a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts
+++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-fields.spec.ts
@@ -4,7 +4,7 @@ import { test, expect } from '../utils/test';
 
 test.use({ storageState: Users.admin.state });
 
-test.describe.serial('omnichannel-agents', () => {
+test.describe.parallel('omnichannel-customFields', () => {
 	let poOmnichannelCustomFields: OmnichannelCustomFields;
 	const newField = 'any_field';
 	test.beforeEach(async ({ page }) => {
@@ -32,14 +32,16 @@ test.describe.serial('omnichannel-agents', () => {
 		await poOmnichannelCustomFields.firstRowInTable(newField).click();
 
 		await poOmnichannelCustomFields.inputLabel.fill('new_any_label');
-		await poOmnichannelCustomFields.btnEditSave.click();
+		await poOmnichannelCustomFields.visibleLabel.click();
+		await poOmnichannelCustomFields.btnSave.click();
 
 		await expect(page.locator(`[qa-user-id="${newField}"] td:nth-child(2)`)).toHaveText(newLabel);
 	});
 
 	test('expect remove "new_field"', async () => {
 		await poOmnichannelCustomFields.inputSearch.fill(newField);
-		await poOmnichannelCustomFields.btnDeletefirstRowInTable.click();
+		await poOmnichannelCustomFields.firstRowInTable(newField).click();
+		await poOmnichannelCustomFields.btnDeleteCustomField.click();
 		await poOmnichannelCustomFields.btnModalRemove.click();
 
 		await poOmnichannelCustomFields.inputSearch.fill(newField);
diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts
index 17430223ca73..9768139fe09d 100644
--- a/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts
+++ b/apps/meteor/tests/e2e/page-objects/omnichannel-custom-fields.ts
@@ -17,31 +17,31 @@ export class OmnichannelCustomFields {
 	}
 
 	get inputField(): Locator {
-		return this.page.locator('[placeholder="Field"]');
+		return this.page.locator('input[name="field"]');
 	}
 
 	get inputLabel(): Locator {
-		return this.page.locator('[placeholder="Label"]');
+		return this.page.locator('input[name="label"]');
+	}
+
+	get visibleLabel(): Locator {
+		return this.page.locator('label >> text="Visible"');
 	}
 
 	get btnSave(): Locator {
-		return this.page.locator('[data-qa-id="NewCustomFieldsPageButtonSave"]');
+		return this.page.locator('button >> text=Save');
 	}
 
 	get inputSearch(): Locator {
 		return this.page.locator('[placeholder="Search"]');
 	}
 
-	get btnEditSave(): Locator {
-		return this.page.locator('[data-qa-id="BtnSaveEditCustomFieldsPage"]');
-	}
-
 	firstRowInTable(filedName: string) {
 		return this.page.locator(`[qa-user-id="${filedName}"]`);
 	}
 
-	get btnDeletefirstRowInTable() {
-		return this.page.locator('button[title="Remove"]');
+	get btnDeleteCustomField() {
+		return this.page.locator('button >> text=Delete');
 	}
 
 	get btnModalRemove(): Locator {
diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts
index 1d2f7a7469b9..494bbde6f4a3 100644
--- a/packages/rest-typings/src/v1/omnichannel.ts
+++ b/packages/rest-typings/src/v1/omnichannel.ts
@@ -3359,7 +3359,7 @@ export type OmnichannelEndpoints = {
 		}>;
 	};
 	'/v1/livechat/custom-fields/:_id': {
-		GET: () => { customField: ILivechatCustomField | null };
+		GET: () => { customField: ILivechatCustomField };
 	};
 	'/v1/livechat/:rid/messages': {
 		GET: (params: LivechatRidMessagesProps) => PaginatedResult<{