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<{