Skip to content

Commit 399ab02

Browse files
feat: Improved Outbound message's modal scrolling experience (RocketChat#36974)
1 parent e748e94 commit 399ab02

23 files changed

Lines changed: 350 additions & 339 deletions

File tree

.changeset/witty-impalas-flow.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/ui-client': minor
3+
'@rocket.chat/meteor': minor
4+
---
5+
6+
Improves the Outbound Message modal’s scrolling on smaller viewports and with large templates

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessagePreview/OutboundMessagePreview.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
ILivechatContact,
77
} from '@rocket.chat/core-typings';
88
import { Box, Margins } from '@rocket.chat/fuselage';
9+
import type { ComponentProps } from 'react';
910
import { useMemo } from 'react';
1011
import { useTranslation } from 'react-i18next';
1112

@@ -14,7 +15,7 @@ import { formatPhoneNumber } from '../../../../../lib/formatPhoneNumber';
1415
import type { TemplateParameters } from '../../definitions/template';
1516
import TemplatePreview from '../TemplatePreview';
1617

17-
type OutboundMessagePreviewProps = {
18+
type OutboundMessagePreviewProps = ComponentProps<typeof Box> & {
1819
template?: IOutboundProviderTemplate;
1920
contactName?: ILivechatContact['name'];
2021
providerName?: IOutboundProviderMetadata['providerName'];
@@ -50,6 +51,7 @@ const OutboundMessagePreview = ({
5051
sender: rawSender,
5152
recipient: rawRecipient,
5253
templateParameters,
54+
...props
5355
}: OutboundMessagePreviewProps) => {
5456
const { t } = useTranslation();
5557

@@ -68,7 +70,7 @@ const OutboundMessagePreview = ({
6870
}, [agentName, agentUsername, departmentName]);
6971

7072
return (
71-
<div>
73+
<Box {...props} is='section'>
7274
<ul>
7375
<Margins blockStart={24}>
7476
<Box is='li' mbs={0}>
@@ -99,7 +101,7 @@ const OutboundMessagePreview = ({
99101
<TemplatePreview template={template} parameters={templateParameters} />
100102
</Box>
101103
) : null}
102-
</div>
104+
</Box>
103105
);
104106
};
105107

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/OutboundMessageWizard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,10 +147,10 @@ const OutboundMessageWizard = ({ defaultValues = {}, onSuccess, onError }: Outbo
147147

148148
return (
149149
<ErrorBoundary fallbackRender={() => <GenericError icon='circle-exclamation' />}>
150-
<Wizard api={wizardApi}>
150+
<Wizard api={wizardApi} display='flex' flexDirection='column' height='100%'>
151151
<WizardTabs />
152152

153-
<Box mbs={16}>
153+
<Box mbs={16} minHeight={0} flexGrow={1}>
154154
<WizardContent id='recipient'>
155155
<RecipientStep defaultValues={state} onDirty={handleDirtyStep} onSubmit={handleSubmit} />
156156
</WizardContent>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Box } from '@rocket.chat/fuselage';
2+
import type { ComponentProps } from 'react';
3+
4+
type OutboundMessageFormProps = ComponentProps<typeof Box> & {
5+
onSubmit?: () => void;
6+
};
7+
8+
const OutboundMessageForm = ({ onSubmit, ...props }: OutboundMessageFormProps) => (
9+
<Box is='form' display='flex' flexDirection='column' height='100%' flexGrow={1} flexShrink={0} onSubmit={onSubmit} {...props} />
10+
);
11+
12+
export default OutboundMessageForm;

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/MessageForm/MessageForm.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IOutboundProviderTemplate, Serialized, ILivechatContact } from '@rocket.chat/core-typings';
2-
import { Box, Button, FieldGroup } from '@rocket.chat/fuselage';
2+
import { Box, Button, FieldGroup, Scrollable } from '@rocket.chat/fuselage';
33
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
44
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
55
import type { ReactNode } from 'react';
@@ -12,6 +12,7 @@ import TemplatePlaceholderField from './components/TemplatePlaceholderField';
1212
import TemplatePreviewForm from './components/TemplatePreviewField';
1313
import type { TemplateParameters } from '../../../../definitions/template';
1414
import { extractParameterMetadata } from '../../../../utils/template';
15+
import Form from '../../components/OutboundMessageForm';
1516
import { FormFetchError } from '../../utils/errors';
1617

1718
export type MessageFormData = {
@@ -78,16 +79,18 @@ const MessageForm = (props: MessageFormProps) => {
7879
});
7980

8081
return (
81-
<form id={messageFormId} onSubmit={handleSubmit(submit)} noValidate>
82-
<FieldGroup>
83-
<TemplateField control={control} templates={templates} onChange={() => setValue('templateParameters', {})} />
82+
<Form id={messageFormId} onSubmit={handleSubmit(submit)} noValidate>
83+
<Scrollable vertical>
84+
<FieldGroup justifyContent='start' pi={2}>
85+
<TemplateField control={control} templates={templates} onChange={() => setValue('templateParameters', {})} />
8486

85-
{parametersMetadata.map((metadata) => (
86-
<TemplatePlaceholderField key={metadata.id} control={control} metadata={metadata} contact={contact} />
87-
))}
87+
{parametersMetadata.map((metadata) => (
88+
<TemplatePlaceholderField key={metadata.id} control={control} metadata={metadata} contact={contact} />
89+
))}
8890

89-
{template ? <TemplatePreviewForm control={control} template={template} /> : null}
90-
</FieldGroup>
91+
{template ? <TemplatePreviewForm control={control} template={template} /> : null}
92+
</FieldGroup>
93+
</Scrollable>
9194

9295
{customActions ?? (
9396
<Box mbs={24} display='flex' justifyContent='end'>
@@ -96,7 +99,7 @@ const MessageForm = (props: MessageFormProps) => {
9699
</Button>
97100
</Box>
98101
)}
99-
</form>
102+
</Form>
100103
);
101104
};
102105

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/MessageForm/components/TemplateField.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IOutboundProviderTemplate, Serialized } from '@rocket.chat/core-typings';
22
import { Field, FieldError, FieldHint, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
33
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
4+
import type { ComponentProps } from 'react';
45
import { useId } from 'react';
56
import type { Control } from 'react-hook-form';
67
import { useController } from 'react-hook-form';
@@ -11,13 +12,13 @@ import TemplateSelect from '../../../../TemplateSelect';
1112
import { cxp } from '../../../utils/cx';
1213
import type { MessageFormData } from '../MessageForm';
1314

14-
type TemplateFieldProps = {
15+
type TemplateFieldProps = ComponentProps<typeof Field> & {
1516
control: Control<MessageFormData>;
1617
templates: Serialized<IOutboundProviderTemplate>[] | undefined;
1718
onChange?: (templateId: string) => void;
1819
};
1920

20-
const TemplateField = ({ control, templates, onChange: onChangeExternal }: TemplateFieldProps) => {
21+
const TemplateField = ({ control, templates, onChange: onChangeExternal, ...props }: TemplateFieldProps) => {
2122
const { t } = useTranslation();
2223
const templateFieldId = useId();
2324

@@ -46,7 +47,7 @@ const TemplateField = ({ control, templates, onChange: onChangeExternal }: Templ
4647
});
4748

4849
return (
49-
<Field>
50+
<Field {...props}>
5051
<FieldLabel is='span' required id={templateFieldId}>
5152
{t('Template')}
5253
</FieldLabel>

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/RecipientForm/RecipientForm.tsx

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IOutboundProviderMetadata, Serialized, ILivechatContact } from '@rocket.chat/core-typings';
2-
import { Box, Button, FieldGroup } from '@rocket.chat/fuselage';
2+
import { Box, Button, FieldGroup, Scrollable } from '@rocket.chat/fuselage';
33
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
44
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
55
import { useEndpoint } from '@rocket.chat/ui-contexts';
@@ -14,6 +14,7 @@ import ContactField from './components/ContactField';
1414
import RecipientField from './components/RecipientField';
1515
import SenderField from './components/SenderField';
1616
import { omnichannelQueryKeys } from '../../../../../../../lib/queryKeys';
17+
import Form from '../../components/OutboundMessageForm';
1718
import { ContactNotFoundError, ProviderNotFoundError } from '../../utils/errors';
1819

1920
export type RecipientFormData = {
@@ -184,43 +185,45 @@ const RecipientForm = (props: RecipientFormProps) => {
184185
});
185186

186187
return (
187-
<form id={recipientFormId} onSubmit={handleSubmit(submit)} noValidate>
188-
<FieldGroup>
189-
<ContactField
190-
control={control}
191-
isError={isErrorContact || isContactNotFound}
192-
isFetching={isFetchingContact}
193-
onRetry={refetchContact}
194-
/>
195-
196-
<ChannelField
197-
control={control}
198-
contact={contact}
199-
disabled={!contactId}
200-
isFetching={isFetchingProvider}
201-
isError={isErrorProvider || isProviderNotFound}
202-
onRetry={refetchProvider}
203-
/>
204-
205-
<RecipientField
206-
control={control}
207-
contact={contact}
208-
type={provider?.providerType || 'phone'}
209-
disabled={!providerId || !contact}
210-
isLoading={isFetchingContact}
211-
/>
212-
213-
<SenderField control={control} provider={provider} disabled={!recipient || !provider} isLoading={isFetchingProvider} />
214-
</FieldGroup>
188+
<Form id={recipientFormId} onSubmit={handleSubmit(submit)} noValidate>
189+
<Scrollable vertical>
190+
<FieldGroup justifyContent='start' pi={2}>
191+
<ContactField
192+
control={control}
193+
isError={isErrorContact || isContactNotFound}
194+
isFetching={isFetchingContact}
195+
onRetry={refetchContact}
196+
/>
197+
198+
<ChannelField
199+
control={control}
200+
contact={contact}
201+
disabled={!contactId}
202+
isFetching={isFetchingProvider}
203+
isError={isErrorProvider || isProviderNotFound}
204+
onRetry={refetchProvider}
205+
/>
206+
207+
<RecipientField
208+
control={control}
209+
contact={contact}
210+
type={provider?.providerType || 'phone'}
211+
disabled={!providerId || !contact}
212+
isLoading={isFetchingContact}
213+
/>
214+
215+
<SenderField control={control} provider={provider} disabled={!recipient || !provider} isLoading={isFetchingProvider} />
216+
</FieldGroup>
217+
</Scrollable>
215218

216219
{customActions ?? (
217-
<Box mbs={24} display='flex' justifyContent='end'>
220+
<Box mbs={24} display='flex' justifyContent='end' flexGrow={0} flexShrink={0}>
218221
<Button type='submit' primary loading={isSubmitting}>
219222
{t('Submit')}
220223
</Button>
221224
</Box>
222225
)}
223-
</form>
226+
</Form>
224227
);
225228
};
226229

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/RepliesForm/RepliesForm.tsx

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Serialized, ILivechatDepartment, ILivechatDepartmentAgents } from '@rocket.chat/core-typings';
2-
import { Box, Button, FieldGroup } from '@rocket.chat/fuselage';
2+
import { Box, Button, FieldGroup, Scrollable } from '@rocket.chat/fuselage';
33
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
44
import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar';
55
import { useEndpoint, usePermission, useUser } from '@rocket.chat/ui-contexts';
@@ -13,6 +13,7 @@ import AgentField from './components/AgentField';
1313
import DepartmentField from './components/DepartmentField';
1414
import { useAllowedAgents } from './hooks/useAllowedAgents';
1515
import { omnichannelQueryKeys } from '../../../../../../../lib/queryKeys';
16+
import Form from '../../components/OutboundMessageForm';
1617
import { FormFetchError } from '../../utils/errors';
1718

1819
export type RepliesFormData = {
@@ -115,25 +116,27 @@ const RepliesForm = (props: RepliesFormProps) => {
115116
});
116117

117118
return (
118-
<form id={repliesFormId} onSubmit={handleSubmit(submit)} noValidate>
119-
<FieldGroup>
120-
<DepartmentField
121-
control={control}
122-
onlyMyDepartments={!canAssignAllDepartments}
123-
isError={isErrorDepartment}
124-
isFetching={isFetchingDepartment}
125-
onRefetch={refetchDepartment}
126-
onChange={() => setValue('agentId', '')}
127-
/>
128-
129-
<AgentField
130-
control={control}
131-
agents={agents}
132-
canAssignAgent={canAssignAnyAgent || canAssignSelfOnlyAgent}
133-
disabled={!departmentId}
134-
isLoading={isFetchingDepartment}
135-
/>
136-
</FieldGroup>
119+
<Form id={repliesFormId} onSubmit={handleSubmit(submit)} noValidate>
120+
<Scrollable vertical>
121+
<FieldGroup justifyContent='start' pi={2}>
122+
<DepartmentField
123+
control={control}
124+
onlyMyDepartments={!canAssignAllDepartments}
125+
isError={isErrorDepartment}
126+
isFetching={isFetchingDepartment}
127+
onRefetch={refetchDepartment}
128+
onChange={() => setValue('agentId', '')}
129+
/>
130+
131+
<AgentField
132+
control={control}
133+
agents={agents}
134+
canAssignAgent={canAssignAnyAgent || canAssignSelfOnlyAgent}
135+
disabled={!departmentId}
136+
isLoading={isFetchingDepartment}
137+
/>
138+
</FieldGroup>
139+
</Scrollable>
137140

138141
{customActions ?? (
139142
<Box mbs={24} display='flex' justifyContent='end'>
@@ -142,7 +145,7 @@ const RepliesForm = (props: RepliesFormProps) => {
142145
</Button>
143146
</Box>
144147
)}
145-
</form>
148+
</Form>
146149
);
147150
};
148151

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/RepliesForm/components/AgentField.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ILivechatDepartmentAgents, Serialized } from '@rocket.chat/core-typings';
22
import { Field, FieldError, FieldHint, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
3+
import type { ComponentProps } from 'react';
34
import { useId } from 'react';
45
import type { Control } from 'react-hook-form';
56
import { useController } from 'react-hook-form';
@@ -9,15 +10,15 @@ import AutoCompleteAgent from '../../../../AutoCompleteDepartmentAgent';
910
import { cxp } from '../../../utils/cx';
1011
import type { RepliesFormData } from '../RepliesForm';
1112

12-
type AgentFieldProps = {
13+
type AgentFieldProps = ComponentProps<typeof Field> & {
1314
control: Control<RepliesFormData>;
1415
agents: Serialized<ILivechatDepartmentAgents>[];
1516
canAssignAgent?: boolean;
1617
disabled?: boolean;
1718
isLoading?: boolean;
1819
};
1920

20-
const AgentField = ({ control, agents, canAssignAgent, disabled = false, isLoading = false }: AgentFieldProps) => {
21+
const AgentField = ({ control, agents, canAssignAgent, disabled = false, isLoading = false, ...props }: AgentFieldProps) => {
2122
const { t } = useTranslation();
2223
const agentFieldId = useId();
2324

@@ -30,7 +31,7 @@ const AgentField = ({ control, agents, canAssignAgent, disabled = false, isLoadi
3031
});
3132

3233
return (
33-
<Field>
34+
<Field {...props}>
3435
<FieldLabel htmlFor={agentFieldId}>{`${t('Agent')} (${t('optional')})`}</FieldLabel>
3536
<FieldRow>
3637
<AutoCompleteAgent

apps/meteor/client/components/Omnichannel/OutboundMessage/components/OutboundMessageWizard/forms/RepliesForm/components/DepartmentField.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Field, FieldError, FieldHint, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
22
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
3+
import type { ComponentProps } from 'react';
34
import { useId } from 'react';
45
import type { Control } from 'react-hook-form';
56
import { useController } from 'react-hook-form';
@@ -10,7 +11,7 @@ import RetryButton from '../../../components/RetryButton';
1011
import { cxp } from '../../../utils/cx';
1112
import type { RepliesFormData } from '../RepliesForm';
1213

13-
type DepartmentFieldProps = {
14+
type DepartmentFieldProps = ComponentProps<typeof Field> & {
1415
control: Control<RepliesFormData>;
1516
onlyMyDepartments?: boolean;
1617
isError: boolean;
@@ -26,6 +27,7 @@ const DepartmentField = ({
2627
isFetching,
2728
onRefetch,
2829
onChange: onChangeExternal,
30+
...props
2931
}: DepartmentFieldProps) => {
3032
const { t } = useTranslation();
3133
const departmentFieldId = useId();
@@ -49,7 +51,7 @@ const DepartmentField = ({
4951
});
5052

5153
return (
52-
<Field>
54+
<Field {...props}>
5355
<FieldLabel is='span' id={departmentFieldId}>{`${t('Department')} (${t('optional')})`}</FieldLabel>
5456
<FieldRow>
5557
<AutoCompleteDepartment

0 commit comments

Comments
 (0)