chore: Replace `useForm` in favor of RHF on Omnichannel Triggers (#30776)

pull/30715/head^2
Douglas Fabris 2 years ago committed by GitHub
parent 5d55a9394e
commit e24af45c8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 31
      apps/meteor/client/components/GenericError/GenericError.tsx
  2. 1
      apps/meteor/client/components/GenericError/index.ts
  3. 2
      apps/meteor/client/views/omnichannel/routes.ts
  4. 306
      apps/meteor/client/views/omnichannel/triggers/EditTrigger.tsx
  5. 104
      apps/meteor/client/views/omnichannel/triggers/EditTriggerPage.js
  6. 25
      apps/meteor/client/views/omnichannel/triggers/EditTriggerPageContainer.js
  7. 28
      apps/meteor/client/views/omnichannel/triggers/EditTriggerWithData.tsx
  8. 94
      apps/meteor/client/views/omnichannel/triggers/NewTriggerPage.js
  9. 40
      apps/meteor/client/views/omnichannel/triggers/TriggersForm.stories.tsx
  10. 212
      apps/meteor/client/views/omnichannel/triggers/TriggersForm.tsx
  11. 48
      apps/meteor/client/views/omnichannel/triggers/TriggersPage.tsx
  12. 17
      apps/meteor/client/views/omnichannel/triggers/TriggersRoute.tsx
  13. 21
      apps/meteor/client/views/omnichannel/triggers/TriggersTable.tsx
  14. 2
      apps/meteor/tests/e2e/omnichannel/omnichannel-triggers.spec.ts
  15. 46
      apps/meteor/tests/e2e/page-objects/omnichannel-triggers.ts
  16. 2
      packages/rest-typings/src/v1/omnichannel.ts

@ -0,0 +1,31 @@
import { Box, States, StatesIcon, StatesTitle, StatesActions, StatesAction } from '@rocket.chat/fuselage';
import type { Keys as IconName } from '@rocket.chat/icons';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
type GenericErrorProps = {
icon?: IconName;
title?: string;
buttonTitle?: string;
buttonAction?: () => void;
};
const GenericError = ({ icon = 'magnifier', title, buttonTitle, buttonAction }: GenericErrorProps) => {
const t = useTranslation();
return (
<Box display='flex' height='100%' flexDirection='column' justifyContent='center'>
<States>
<StatesIcon name={icon} variation='danger' />
<StatesTitle>{title || t('Something_went_wrong')}</StatesTitle>
{buttonAction && (
<StatesActions>
<StatesAction onClick={buttonAction}>{buttonTitle || t('Reload_page')}</StatesAction>
</StatesActions>
)}
</States>
</Box>
);
};
export default GenericError;

@ -0,0 +1 @@
export { default } from './GenericError';

@ -128,7 +128,7 @@ registerOmnichannelRoute('/tags/:context?/:id?', {
registerOmnichannelRoute('/triggers/:context?/:id?', {
name: 'omnichannel-triggers',
component: lazy(() => import('./triggers/TriggersPage')),
component: lazy(() => import('./triggers/TriggersRoute')),
});
registerOmnichannelRoute('/current/:id?/:tab?/:context?', {

@ -0,0 +1,306 @@
import type { ILivechatTrigger, ILivechatTriggerCondition, Serialized } from '@rocket.chat/core-typings';
import type { SelectOption } from '@rocket.chat/fuselage';
import {
FieldGroup,
Button,
ButtonGroup,
Box,
Field,
FieldLabel,
FieldRow,
FieldError,
TextInput,
ToggleSwitch,
Select,
TextAreaInput,
} from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRouter, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import React, { useMemo } from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import {
ContextualbarScrollableContent,
ContextualbarTitle,
ContextualbarFooter,
Contextualbar,
ContextualbarHeader,
ContextualbarClose,
} from '../../../components/Contextualbar';
const getInitialValues = (triggerData: Serialized<ILivechatTrigger> | undefined) => ({
name: triggerData?.name || '',
description: triggerData?.description || '',
enabled: triggerData?.enabled || true,
runOnce: !!triggerData?.runOnce || false,
conditions: triggerData?.conditions.map(({ name, value }) => ({ name: name || 'page-url', value: value || '' })) || [
{ name: 'page-url' as unknown as ILivechatTriggerCondition['name'], value: '' },
],
actions: triggerData?.actions.map(({ name, params }) => ({
name: name || '',
params: {
sender: params?.sender || 'queue',
msg: params?.msg || '',
name: params?.name || '',
},
})) || [
{
name: 'send-message',
params: {
sender: 'queue',
msg: '',
name: '',
},
},
],
});
type TriggersPayload = {
name: string;
description: string;
enabled: boolean;
runOnce: boolean;
// In the future, this will be an array
conditions: ILivechatTrigger['conditions'];
// In the future, this will be an array
actions: ILivechatTrigger['actions'];
};
const EditTrigger = ({ triggerData }: { triggerData?: Serialized<ILivechatTrigger> }) => {
const t = useTranslation();
const router = useRouter();
const queryClient = useQueryClient();
const dispatchToastMessage = useToastMessageDispatch();
const saveTrigger = useEndpoint('POST', '/v1/livechat/triggers');
const {
control,
handleSubmit,
formState: { isDirty, errors },
watch,
} = useForm<TriggersPayload>({ mode: 'onBlur', values: getInitialValues(triggerData) });
const { fields: conditionsFields } = useFieldArray({
control,
name: 'conditions',
});
const { fields: actionsFields } = useFieldArray({
control,
name: 'actions',
});
const { description, conditions, actions } = watch();
const conditionOptions: SelectOption[] = useMemo(
() => [
['page-url', t('Visitor_page_URL')],
['time-on-site', t('Visitor_time_on_site')],
['chat-opened-by-visitor', t('Chat_opened_by_visitor')],
['after-guest-registration', t('After_guest_registration')],
],
[t],
);
const conditionValuePlaceholders: { [conditionName: string]: string } = useMemo(
() => ({
'page-url': t('Enter_a_regex'),
'time-on-site': t('Time_in_seconds'),
}),
[t],
);
const senderOptions: SelectOption[] = useMemo(
() => [
['queue', t('Impersonate_next_agent_from_queue')],
['custom', t('Custom_agent')],
],
[t],
);
const saveTriggerMutation = useMutation({
mutationFn: saveTrigger,
onSuccess: () => {
dispatchToastMessage({ type: 'success', message: t('Saved') });
queryClient.invalidateQueries(['livechat-triggers']);
router.navigate('/omnichannel/triggers');
},
onError: (error) => {
dispatchToastMessage({ type: 'error', message: error });
},
});
const handleSave = async (data: TriggersPayload) => {
saveTriggerMutation.mutateAsync({ ...data, _id: triggerData?._id });
};
const formId = useUniqueId();
const enabledField = useUniqueId();
const runOnceField = useUniqueId();
const nameField = useUniqueId();
const descriptionField = useUniqueId();
const conditionField = useUniqueId();
const actionField = useUniqueId();
const actionMessageField = useUniqueId();
return (
<Contextualbar>
<ContextualbarHeader>
<ContextualbarTitle>{triggerData?._id ? t('Edit_Trigger') : t('New_Trigger')}</ContextualbarTitle>
<ContextualbarClose onClick={() => router.navigate('/omnichannel/triggers')} />
</ContextualbarHeader>
<ContextualbarScrollableContent>
<form id={formId} onSubmit={handleSubmit(handleSave)}>
<FieldGroup>
<Field>
<Box display='flex' flexDirection='row'>
<FieldLabel htmlFor={enabledField}>{t('Enabled')}</FieldLabel>
<FieldRow>
<Controller
name='enabled'
control={control}
render={({ field: { value, ...field } }) => <ToggleSwitch id={enabledField} {...field} checked={value} />}
/>
</FieldRow>
</Box>
</Field>
<Field>
<Box display='flex' flexDirection='row'>
<FieldLabel htmlFor={runOnceField}>{t('Run_only_once_for_each_visitor')}</FieldLabel>
<FieldRow>
<Controller
name='runOnce'
control={control}
render={({ field: { value, ...field } }) => <ToggleSwitch id={runOnceField} {...field} checked={value} />}
/>
</FieldRow>
</Box>
</Field>
<Field>
<FieldLabel htmlFor={nameField} required>
{t('Name')}
</FieldLabel>
<FieldRow>
<Controller
name='name'
control={control}
rules={{ required: t('The_field_is_required', t('Name')) }}
render={({ field }) => (
<TextInput
{...field}
id={nameField}
error={errors?.name?.message}
aria-required={true}
aria-invalid={Boolean(errors?.name)}
aria-describedby={`${nameField}-error`}
/>
)}
/>
</FieldRow>
{errors?.name && (
<FieldError aria-live='assertive' id={`${nameField}-error`}>
{errors?.name.message}
</FieldError>
)}
</Field>
<Field>
<FieldLabel htmlFor={descriptionField}>{t('Description')}</FieldLabel>
<FieldRow>
<Controller
name='description'
control={control}
render={({ field }) => <TextInput id={descriptionField} {...field} value={description} />}
/>
</FieldRow>
</Field>
{conditionsFields.map((_, index) => {
const conditionValuePlaceholder = conditionValuePlaceholders[conditions[index].name];
return (
<Field key={index}>
<FieldLabel htmlFor={conditionField}>{t('Condition')}</FieldLabel>
<FieldRow>
<Controller
name={`conditions.${index}.name`}
control={control}
render={({ field }) => <Select id={conditionField} {...field} options={conditionOptions} />}
/>
</FieldRow>
{conditionValuePlaceholder && (
<FieldRow>
<Controller
name={`conditions.${index}.value`}
control={control}
render={({ field }) => <TextInput {...field} placeholder={conditionValuePlaceholder} />}
/>
</FieldRow>
)}
</Field>
);
})}
{actionsFields.map((_, index) => (
<Field key={index}>
<FieldLabel htmlFor={actionField}>{t('Action')}</FieldLabel>
<FieldRow>
<TextInput value={t('Send_a_message')} readOnly />
</FieldRow>
<FieldRow>
<Controller
name={`actions.${index}.params.sender`}
control={control}
render={({ field }) => (
<Select id={actionField} {...field} options={senderOptions} placeholder={t('Select_an_option')} />
)}
/>
</FieldRow>
{actions[index].params?.sender === 'custom' && (
<FieldRow>
<Controller
name={`actions.${index}.params.name`}
control={control}
render={({ field }) => <TextInput {...field} placeholder={t('Name_of_agent')} />}
/>
</FieldRow>
)}
<FieldRow>
<Controller
name={`actions.${index}.params.msg`}
control={control}
rules={{ required: t('The_field_is_required', t('Message')) }}
render={({ field }) => (
<TextAreaInput
error={errors.actions?.[index]?.params?.msg?.message}
aria-invalid={Boolean(errors.actions?.[index]?.params?.msg)}
aria-describedby={`${actionMessageField}-error`}
aria-required={true}
{...field}
rows={3}
placeholder={`${t('Message')}*`}
/>
)}
/>
</FieldRow>
{errors.actions?.[index]?.params?.msg && (
<FieldError aria-live='assertive' id={`${actionMessageField}-error`}>
{errors.actions?.[index]?.params?.msg?.message}
</FieldError>
)}
</Field>
))}
</FieldGroup>
</form>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button onClick={() => router.navigate('/omnichannel/triggers')}>{t('Cancel')}</Button>
<Button form={formId} type='submit' primary disabled={!isDirty}>
{t('Save')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</Contextualbar>
);
};
export default EditTrigger;

@ -1,104 +0,0 @@
import { FieldGroup, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar';
import { useForm } from '../../../hooks/useForm';
import TriggersForm from './TriggersForm';
const getInitialValues = ({
name,
description,
enabled,
runOnce,
conditions: [{ name: condName, value: condValue }],
actions: [
{
action: actName,
params: { sender: actSender, msg: actMsg, name: actSenderName },
},
],
}) => ({
name: name ?? '',
description: description ?? '',
enabled: !!enabled,
runOnce: !!runOnce,
conditions: {
name: condName ?? 'page-url',
value: condValue ?? '',
},
actions: {
name: actName ?? '',
params: {
sender: actSender ?? 'queue',
msg: actMsg ?? '',
name: actSenderName ?? '',
},
},
});
const EditTriggerPage = ({ data, onSave }) => {
const dispatchToastMessage = useToastMessageDispatch();
const t = useTranslation();
const router = useRoute('omnichannel-triggers');
const save = useEndpoint('POST', '/v1/livechat/triggers');
const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data));
const handleSave = useMutableCallback(async () => {
try {
const {
actions: {
params: { sender, msg, name },
},
...restValues
} = values;
await save({
_id: data._id,
...restValues,
conditions: [values.conditions],
actions: [
{
name: 'send-message',
params: {
sender,
msg,
...(sender === 'custom' && { name }),
},
},
],
});
dispatchToastMessage({ type: 'success', message: t('Saved') });
onSave();
router.push({});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const { name } = values;
const canSave = name && hasUnsavedChanges;
return (
<>
<ContextualbarScrollableContent>
<FieldGroup>
<TriggersForm values={values} handlers={handlers} />
</FieldGroup>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button primary onClick={handleSave} disabled={!canSave}>
{t('Save')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</>
);
};
export default EditTriggerPage;

@ -1,25 +0,0 @@
import { Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import PageSkeleton from '../../../components/PageSkeleton';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
import { useEndpointData } from '../../../hooks/useEndpointData';
import EditTriggerPage from './EditTriggerPage';
const EditTriggerPageContainer = ({ id, onSave }) => {
const t = useTranslation();
const { value: data, phase: state } = useEndpointData('/v1/livechat/triggers/:_id', { keys: { _id: id } });
if (state === AsyncStatePhase.LOADING) {
return <PageSkeleton />;
}
if (state === AsyncStatePhase.REJECTED || !data?.trigger) {
return <Callout>{t('Error')}: error</Callout>;
}
return <EditTriggerPage data={data.trigger} onSave={onSave} />;
};
export default EditTriggerPageContainer;

@ -0,0 +1,28 @@
import type { ILivechatTrigger } 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 { ContextualbarSkeleton } from '../../../components/Contextualbar';
import EditTrigger from './EditTrigger';
const EditTriggerWithData = ({ triggerId }: { triggerId: ILivechatTrigger['_id'] }) => {
const t = useTranslation();
const getTriggersById = useEndpoint('GET', '/v1/livechat/triggers/:_id', { _id: triggerId });
const { data, isLoading, isError } = useQuery(['livechat-getTriggersById', triggerId], async () => getTriggersById(), {
refetchOnWindowFocus: false,
});
if (isLoading) {
return <ContextualbarSkeleton />;
}
if (isError) {
return <Callout>{t('Error')}</Callout>;
}
return <EditTrigger triggerData={data.trigger} />;
};
export default EditTriggerWithData;

@ -1,94 +0,0 @@
import { Button, FieldGroup, ButtonGroup } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useToastMessageDispatch, useRoute, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useMemo } from 'react';
import { ContextualbarScrollableContent, ContextualbarFooter } from '../../../components/Contextualbar';
import { useForm } from '../../../hooks/useForm';
import TriggersForm from './TriggersForm';
const NewTriggerPage = ({ onSave }) => {
const dispatchToastMessage = useToastMessageDispatch();
const t = useTranslation();
const router = useRoute('omnichannel-triggers');
const save = useEndpoint('POST', '/v1/livechat/triggers');
const { values, handlers } = useForm({
name: '',
description: '',
enabled: true,
runOnce: false,
conditions: {
name: 'page-url',
value: '',
},
actions: {
name: '',
params: {
sender: 'queue',
msg: '',
name: '',
},
},
});
const handleSave = useMutableCallback(async () => {
try {
const {
actions: {
params: { sender, msg, name },
},
...restValues
} = values;
await save({
...restValues,
conditions: [values.conditions],
actions: [
{
name: 'send-message',
params: {
sender,
msg,
...(sender === 'custom' && { name }),
},
},
],
});
dispatchToastMessage({ type: 'success', message: t('Saved') });
onSave();
router.push({});
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
const {
name,
actions: {
params: { msg },
},
} = values;
const canSave = useMemo(() => name && msg, [name, msg]);
return (
<>
<ContextualbarScrollableContent>
<FieldGroup>
<TriggersForm values={values} handlers={handlers} />
</FieldGroup>
</ContextualbarScrollableContent>
<ContextualbarFooter>
<ButtonGroup stretch>
<Button primary onClick={handleSave} disabled={!canSave}>
{t('Save')}
</Button>
</ButtonGroup>
</ContextualbarFooter>
</>
);
};
export default NewTriggerPage;

@ -1,13 +1,12 @@
import { FieldGroup, Box } from '@rocket.chat/fuselage';
import { action } from '@storybook/addon-actions';
import type { ComponentMeta, ComponentStory } from '@storybook/react';
import React from 'react';
import TriggersForm from './TriggersForm';
import EditTrigger from './EditTrigger';
export default {
title: 'Omnichannel/TriggersForm',
component: TriggersForm,
title: 'Omnichannel/EditTrigger',
component: EditTrigger,
decorators: [
(fn) => (
<Box maxWidth='x600'>
@ -15,35 +14,6 @@ export default {
</Box>
),
],
} as ComponentMeta<typeof TriggersForm>;
} as ComponentMeta<typeof EditTrigger>;
export const Default: ComponentStory<typeof TriggersForm> = (args) => <TriggersForm {...args} />;
Default.storyName = 'TriggersForm';
Default.args = {
values: {
name: '',
description: '',
enabled: true,
runOnce: false,
conditions: {
name: 'page-url',
value: '',
},
actions: {
name: '',
params: {
sender: 'queue',
msg: '',
name: '',
},
},
},
handlers: {
handleName: action('handleName'),
handleDescription: action('handleDescription'),
handleEnabled: action('handleEnabled'),
handleRunOnce: action('handleRunOnce'),
handleConditions: action('handleConditions'),
handleActions: action('handleActions'),
},
};
export const Default: ComponentStory<typeof EditTrigger> = (args) => <EditTrigger {...args} />;

@ -1,212 +0,0 @@
import type { SelectOption } from '@rocket.chat/fuselage';
import { Box, Field, FieldLabel, FieldRow, FieldError, TextInput, ToggleSwitch, Select, TextAreaInput } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ComponentProps, FC, FormEvent } from 'react';
import React, { useMemo, useState } from 'react';
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate';
type TriggerConditions = {
name: string;
value: string | number;
};
type TriggerActions = {
name: string;
params: {
sender: string | undefined;
msg: string;
name: string;
};
};
type TriggersFormProps = {
values: {
name: string;
description: string;
enabled: boolean;
runOnce: boolean;
// In the future, this will be an array
conditions: TriggerConditions;
// In the future, this will be an array
actions: TriggerActions;
};
handlers: {
handleName: (event: FormEvent<HTMLInputElement>) => void;
handleDescription: (event: FormEvent<HTMLInputElement>) => void;
handleEnabled: (event: FormEvent<HTMLInputElement>) => void;
handleRunOnce: (event: FormEvent<HTMLInputElement>) => void;
handleConditions: (value: TriggerConditions) => void;
handleActions: (value: TriggerActions) => void;
};
className?: ComponentProps<typeof Field>['className'];
};
const TriggersForm: FC<TriggersFormProps> = ({ values, handlers, className }) => {
const [nameError, setNameError] = useState('');
const [msgError, setMsgError] = useState('');
const t = useTranslation();
const { name, description, enabled, runOnce, conditions, actions } = values;
const { handleName, handleDescription, handleEnabled, handleRunOnce, handleConditions, handleActions } = handlers;
const { name: conditionName, value: conditionValue } = conditions;
const {
params: { sender: actionSender, msg: actionMsg, name: actionAgentName },
} = actions;
const conditionOptions: SelectOption[] = useMemo(
() => [
['page-url', t('Visitor_page_URL')],
['time-on-site', t('Visitor_time_on_site')],
['chat-opened-by-visitor', t('Chat_opened_by_visitor')],
['after-guest-registration', t('After_guest_registration')],
],
[t],
);
const conditionValuePlaceholders: { [conditionName: string]: string } = useMemo(
() => ({
'page-url': t('Enter_a_regex'),
'time-on-site': t('Time_in_seconds'),
}),
[t],
);
const conditionValuePlaceholder = conditionValuePlaceholders[conditionName];
const senderOptions: SelectOption[] = useMemo(
() => [
['queue', t('Impersonate_next_agent_from_queue')],
['custom', t('Custom_agent')],
],
[t],
);
const handleConditionName = useMutableCallback((name) => {
handleConditions({
name,
value: '',
});
});
const handleConditionValue = useMutableCallback(({ currentTarget: { value } }) => {
handleConditions({
...conditions,
value,
});
});
const handleActionAgentName = useMutableCallback(({ currentTarget: { value: name } }) => {
handleActions({
...actions,
params: {
...actions.params,
name,
},
});
});
const handleActionSender = useMutableCallback((sender) => {
handleActions({
...actions,
params: {
...actions.params,
sender,
},
});
});
const handleActionMessage = useMutableCallback(({ currentTarget: { value: msg } }) => {
handleActions({
...actions,
params: {
...actions.params,
msg,
},
});
});
useComponentDidUpdate(() => {
setNameError(!name ? t('The_field_is_required', t('Name')) : '');
}, [t, name]);
useComponentDidUpdate(() => {
setMsgError(!actionMsg ? t('The_field_is_required', t('Message')) : '');
}, [t, actionMsg]);
return (
<>
<Field className={className}>
<Box display='flex' flexDirection='row'>
<FieldLabel>{t('Enabled')}</FieldLabel>
<FieldRow>
<ToggleSwitch checked={enabled} onChange={handleEnabled} />
</FieldRow>
</Box>
</Field>
<Field className={className}>
<Box display='flex' flexDirection='row'>
<FieldLabel>{t('Run_only_once_for_each_visitor')}</FieldLabel>
<FieldRow>
<ToggleSwitch checked={runOnce} onChange={handleRunOnce} />
</FieldRow>
</Box>
</Field>
<Field className={className}>
<FieldLabel>{t('Name')}*</FieldLabel>
<FieldRow>
<TextInput value={name} error={nameError} onChange={handleName} placeholder={t('Name')} />
</FieldRow>
<FieldError>{nameError}</FieldError>
</Field>
<Field className={className}>
<FieldLabel>{t('Description')}</FieldLabel>
<FieldRow>
<TextInput value={description} onChange={handleDescription} placeholder={t('Description')} />
</FieldRow>
</Field>
<Field className={className}>
<FieldLabel>{t('Condition')}</FieldLabel>
<FieldRow>
<Select name='condition' options={conditionOptions} value={conditionName} onChange={handleConditionName} />
</FieldRow>
{conditionValuePlaceholder && (
<FieldRow>
<TextInput
name='conditionValue'
value={conditionValue}
onChange={handleConditionValue}
placeholder={conditionValuePlaceholder}
/>
</FieldRow>
)}
</Field>
<Field className={className}>
<FieldLabel>{t('Action')}</FieldLabel>
<FieldRow>
<TextInput value={t('Send_a_message')} disabled />
</FieldRow>
<FieldRow>
<Select
name='sender'
options={senderOptions}
value={actionSender}
onChange={handleActionSender}
placeholder={t('Select_an_option')}
/>
</FieldRow>
{actionSender === 'custom' && (
<FieldRow>
<TextInput name='agentName' value={actionAgentName} onChange={handleActionAgentName} placeholder={t('Name_of_agent')} />
</FieldRow>
)}
<FieldRow>
<TextAreaInput name='triggerMessage' rows={3} value={actionMsg} onChange={handleActionMessage} placeholder={`${t('Message')}*`} />
</FieldRow>
<FieldError>{msgError}</FieldError>
</Field>
</>
);
};
export default TriggersForm;

@ -1,60 +1,30 @@
import { Button } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useRouteParameter, usePermission, useTranslation } from '@rocket.chat/ui-contexts';
import React, { useRef, useCallback } from 'react';
import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';
import { Contextualbar, ContextualbarTitle, ContextualbarHeader, ContextualbarClose } from '../../../components/Contextualbar';
import Page from '../../../components/Page';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import EditTriggerPageContainer from './EditTriggerPageContainer';
import NewTriggerPage from './NewTriggerPage';
import EditTrigger from './EditTrigger';
import EditTriggerWithData from './EditTriggerWithData';
import TriggersTable from './TriggersTable';
const TriggersPage = () => {
const t = useTranslation();
const id = useRouteParameter('id');
const context = useRouteParameter('context');
const router = useRoute('omnichannel-triggers');
const canViewTriggers = usePermission('view-livechat-triggers');
const reload = useRef(() => null);
const handleReload = useCallback(() => {
reload.current();
}, []);
const handleAdd = useMutableCallback(() => {
router.push({ context: 'new' });
});
const handleCloseContextualbar = useMutableCallback(() => {
router.push({});
});
if (!canViewTriggers) {
return <NotAuthorizedPage />;
}
const router = useRouter();
return (
<Page flexDirection='row'>
<Page>
<Page.Header title={t('Livechat_Triggers')}>
<Button onClick={handleAdd}>{t('Create_trigger')}</Button>
<Button onClick={() => router.navigate('/omnichannel/triggers/new')}>{t('Create_trigger')}</Button>
</Page.Header>
<Page.Content>
<TriggersTable reload={reload} />
<TriggersTable />
</Page.Content>
</Page>
{context && (
<Contextualbar>
<ContextualbarHeader>
<ContextualbarTitle>{t('Trigger')}</ContextualbarTitle>
<ContextualbarClose onClick={handleCloseContextualbar} />
</ContextualbarHeader>
{context === 'edit' && <EditTriggerPageContainer key={id} id={id} onSave={handleReload} />}
{context === 'new' && <NewTriggerPage onSave={handleReload} />}
</Contextualbar>
)}
{context === 'edit' && id && <EditTriggerWithData triggerId={id} />}
{context === 'new' && <EditTrigger />}
</Page>
);
};

@ -0,0 +1,17 @@
import { usePermission } from '@rocket.chat/ui-contexts';
import React from 'react';
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage';
import TriggersPage from './TriggersPage';
const TriggersRoute = () => {
const canViewTriggers = usePermission('view-livechat-triggers');
if (!canViewTriggers) {
return <NotAuthorizedPage />;
}
return <TriggersPage />;
};
export default TriggersRoute;

@ -1,11 +1,11 @@
import { Callout, Pagination } from '@rocket.chat/fuselage';
import { Pagination } from '@rocket.chat/fuselage';
import { 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, { useEffect, useMemo, useState } from 'react';
import React, { useMemo, useState } from 'react';
import GenericNoResults from '../../../components/GenericNoResults/GenericNoResults';
import GenericError from '../../../components/GenericError';
import GenericNoResults from '../../../components/GenericNoResults';
import {
GenericTable,
GenericTableHeader,
@ -16,7 +16,7 @@ import {
import { usePagination } from '../../../components/GenericTable/hooks/usePagination';
import TriggersRow from './TriggersRow';
const TriggersTable = ({ reload }: { reload: MutableRefObject<() => void> }) => {
const TriggersTable = () => {
const t = useTranslation();
const router = useRouter();
@ -34,20 +34,12 @@ const TriggersTable = ({ reload }: { reload: MutableRefObject<() => void> }) =>
const [defaultQuery] = useState(hashQueryKey([query]));
const queryHasChanged = defaultQuery !== hashQueryKey([query]);
useEffect(() => {
reload.current = refetch;
}, [reload, refetch]);
if (isError) {
return <Callout>{t('Error')}</Callout>;
}
const headers = (
<>
<GenericTableHeaderCell>{t('Name')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Description')}</GenericTableHeaderCell>
<GenericTableHeaderCell>{t('Enabled')}</GenericTableHeaderCell>
<GenericTableHeaderCell width='x60'>{t('Remove')}</GenericTableHeaderCell>
<GenericTableHeaderCell width='x60' />
</>
);
@ -94,6 +86,7 @@ const TriggersTable = ({ reload }: { reload: MutableRefObject<() => void> }) =>
/>
</>
)}
{isError && <GenericError buttonAction={refetch} />}
</>
);
};

@ -6,7 +6,7 @@ import { Users } from '../fixtures/userStates';
import { OmnichannelLiveChat, HomeOmnichannel } from '../page-objects';
import { test, expect } from '../utils/test';
test.describe.serial('Omnichannel Triggers', () => {
test.describe.parallel('omnichannel-triggers', () => {
let triggersName: string;
let triggerMessage: string;
let poLiveChat: OmnichannelLiveChat;

@ -17,33 +17,17 @@ export class OmnichannelTriggers {
}
get inputName(): Locator {
return this.page.locator('[placeholder="Name"]');
return this.page.locator('input[name="name"]');
}
get inputDescription(): Locator {
return this.page.locator('[placeholder="Description"]');
}
get addTime(): Locator {
return this.page.locator('[placeholder="Time in seconds"]');
}
get impersonateAgentListBox(): Locator {
return this.page.locator('ol[role="listbox"] >> text=Impersonate next agent from queue');
}
get textArea(): Locator {
return this.page.locator('textarea');
return this.page.locator('input[name="description"]');
}
get btnSave(): Locator {
return this.page.locator('button >> text="Save"');
}
get firstRowInTable() {
return this.page.locator('table tr:first-child td:first-child');
}
firstRowInTriggerTable(triggersName1: string) {
return this.page.locator(`text="${triggersName1}"`);
}
@ -56,14 +40,6 @@ export class OmnichannelTriggers {
return this.toastMessage.locator('role=button');
}
get inputSearch() {
return this.page.locator('[placeholder="Search"]');
}
get pageTitle() {
return this.page.locator('[data-qa-type="PageHeader-title"]');
}
get btnDeletefirstRowInTable() {
return this.page.locator('table tr:first-child td:last-child button');
}
@ -76,33 +52,33 @@ export class OmnichannelTriggers {
return this.page.locator('text=Trigger removed');
}
get inputCondition(): Locator {
return this.page.locator('button', { has: this.page.locator('select[name="condition"]') });
get conditionLabel(): Locator {
return this.page.locator('label >> text="Condition"')
}
get inputConditionValue(): Locator {
return this.page.locator('input[name="conditionValue"]');
return this.page.locator('input[name="conditions.0.value"]');
}
get inputSender(): Locator {
return this.page.locator('button', { has: this.page.locator('select[name="sender"]') });
get actionLabel(): Locator {
return this.page.locator('label >> text="Action"')
}
get inputAgentName(): Locator {
return this.page.locator('input[name="agentName"]');
return this.page.locator('input[name="actions.0.params.name"]');
}
get inputTriggerMessage(): Locator {
return this.page.locator('textarea[name="triggerMessage"]');
return this.page.locator('textarea[name="actions.0.params.msg"]');
}
async selectCondition(condition: string) {
await this.inputCondition.click();
await this.conditionLabel.click();
await this.page.locator(`li.rcx-option[data-key="${condition}"]`).click();
}
async selectSender(sender: 'queue' | 'custom') {
await this.inputSender.click();
await this.actionLabel.click();
await this.page.locator(`li.rcx-option[data-key="${sender}"]`).click();
}

@ -3615,7 +3615,7 @@ export type OmnichannelEndpoints = {
POST: (params: POSTLivechatTriggersParams) => void;
};
'/v1/livechat/triggers/:_id': {
GET: () => { trigger: ILivechatTrigger | null };
GET: () => { trigger: ILivechatTrigger };
DELETE: () => void;
};
'/v1/livechat/rooms': {

Loading…
Cancel
Save