-
Notifications
You must be signed in to change notification settings - Fork 13k
feat: Outbound Message UI #36207
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: Outbound Message UI #36207
Changes from all commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
f6cd389
chore: mock functions
aleksandernsilva 172bb9e
feat: outbound-message license module
aleksandernsilva c255666
feat: formatPhoneNumber utility function
aleksandernsilva 9beeecf
feat: OutboundMessageWizard
aleksandernsilva f1f2914
chore: OutboundMessageWizard storybook
aleksandernsilva 1d9211a
feat: OutboundMessageModal
aleksandernsilva b00641c
feat: outbound message action
aleksandernsilva 93756d9
feat: Outbound Message: Message Step (#36370)
aleksandernsilva 49744e0
feat: Outbound Message: Replies Step (#36371)
aleksandernsilva b186132
feat: Outbound Message: Review Step (#36372)
aleksandernsilva edfb8ec
feat: Outbound Message Upsell Modal (#36335)
aleksandernsilva 31e45a6
feat: Outbound Message: Recipient Step (#36369)
aleksandernsilva 4683f95
chore: removed mocks
aleksandernsilva 7c6ddd1
chore: adjusted mock
aleksandernsilva 628422e
refactor: Outbound Message Recipient Step Improvements (#36654)
aleksandernsilva 86edd4c
feat: Outbound message template improvements (#36711)
aleksandernsilva 63bb859
feat: Outbound message form improvements (#36713)
aleksandernsilva 3320e9b
refactor: Replaced agent fetch with department agents and added permi…
aleksandernsilva 5a33bd1
fix: Outbound message upsell modal not being displayed when there's n…
aleksandernsilva e529279
fix: Template placeholder not being used as fallback when parameter i…
aleksandernsilva 01477f0
feat(outbound): Added language description to template select compone…
aleksandernsilva 23fb359
feat(outbound): Contact info shortcuts (#36666)
aleksandernsilva 460e216
feat(outbound): UI permissions (#36794)
aleksandernsilva 0f5258e
feat(outbound): Send message logic (#36631)
aleksandernsilva 583f174
i18n: Outbound message pt-BR and en-US translations (#36878)
aleksandernsilva df32872
fix(outbound): agent autocomplete displaying incorrect avatar (#36892)
aleksandernsilva 7096bf0
feat(outbound): Added required validation for template placeholder fi…
aleksandernsilva 74e915f
refactor(outbound): recipient form fields (#36899)
aleksandernsilva 44a26ce
fix: reading length from undefined
aleksandernsilva 4a08211
feat: added default values to template placeholder fields
aleksandernsilva 6677cc1
fix(outbound): Agents not being sorted alphabetically (#36901)
aleksandernsilva b47518b
fix(outbound): Contact name getting truncated by the phone number des…
aleksandernsilva 440ffcd
fix: selected agent incorrect avatar
aleksandernsilva 5058344
fix(outbound): Placeholder input not being focused after menu is clos…
aleksandernsilva ed9b6c6
fix(outbound): Upsell modal conditions (#36930)
aleksandernsilva ebb221a
fix(outbound): Clear placeholder once an agent is selected (#36939)
aleksandernsilva e1eefc8
refactor: removed providers fetch default stale time
aleksandernsilva 508630e
refactor: adjusted send message error handling
aleksandernsilva 92af2d2
chore: adjusted mocks
aleksandernsilva 16e8290
chore: improved AutoCompleteContact typing
aleksandernsilva aeacd62
chore: changed storybook decorators to array
aleksandernsilva f4305e4
fix: field error and description mismatch
aleksandernsilva 4018640
refactor: added provider type to phone number validation
aleksandernsilva 5b066ba
test: corrected types and payload expectation
aleksandernsilva 3c34dfc
test: added missing import
aleksandernsilva e162414
refactor: adjusted TemplatePlaceholderSelector typing
aleksandernsilva 17f2f0a
feat: adjusted providers query key to include type
aleksandernsilva 94de8f6
refactor: improved ContactInforOutboundMessageButton typing
aleksandernsilva 8028bce
refactor: improved contact info submit logic
aleksandernsilva 6567243
refactor: improved template utility functions
aleksandernsilva f3c4b21
chore: adjusted AutoCompletContact typing
aleksandernsilva c457d76
chore: reviews
aleksandernsilva 3da023d
chore: bump fuselage
aleksandernsilva 8895155
feat(outbound): Added closing confirmation to the modal (#36937)
aleksandernsilva 88d1374
chore: changeset
aleksandernsilva ba6e3f9
i18n: normalized "Outbound Message" to "Outbound message"
aleksandernsilva b7b0542
feat: added close button to close confirmation modal
aleksandernsilva 26a9371
chore: updated changeset
aleksandernsilva 8b35745
refactor: improved contacts query keys
aleksandernsilva 2c0f847
refactor: added missing key
aleksandernsilva 74bfac9
feat: added documentation links
aleksandernsilva 78a076a
test: adjusted OutboundMessageModal unit tests
aleksandernsilva 4b9b1b8
test: adjusted doc links
aleksandernsilva 1c12524
Merge branch 'develop' into feat/outbound-msg-ui
kodiakhq[bot] b6fcc40
Merge branch 'develop' into feat/outbound-msg-ui
kodiakhq[bot] File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| '@rocket.chat/web-ui-registration': patch | ||
| '@rocket.chat/storybook-config': patch | ||
| '@rocket.chat/fuselage-ui-kit': patch | ||
| '@rocket.chat/ui-theming': patch | ||
| '@rocket.chat/ui-video-conf': patch | ||
| '@rocket.chat/uikit-playground': patch | ||
| '@rocket.chat/ui-composer': patch | ||
| '@rocket.chat/gazzodown': patch | ||
| '@rocket.chat/ui-avatar': patch | ||
| '@rocket.chat/ui-client': patch | ||
| '@rocket.chat/ui-voip': patch | ||
| '@rocket.chat/core-typings': minor | ||
| '@rocket.chat/apps-engine': minor | ||
| '@rocket.chat/license': minor | ||
| '@rocket.chat/i18n': minor | ||
| '@rocket.chat/meteor': minor | ||
| --- | ||
|
|
||
| Introduces the Outbound Message feature to Omnichannel, allowing organizations to initiate proactive communication with contacts through their preferred messaging channel directly from Rocket.Chat |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
63 changes: 63 additions & 0 deletions
63
apps/meteor/client/components/AutoCompleteContact/AutoCompleteContact.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| import type { Serialized } from '@rocket.chat/core-typings'; | ||
| import { PaginatedSelectFiltered } from '@rocket.chat/fuselage'; | ||
| import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
| import type { ILivechatContactWithManagerData } from '@rocket.chat/rest-typings'; | ||
| import type { ComponentProps, ReactElement, SyntheticEvent } from 'react'; | ||
| import { memo, useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useContactsList } from './useContactsList'; | ||
|
|
||
| type OptionProps = { | ||
| role: 'option'; | ||
| title?: string; | ||
| index: number; | ||
| label: string; | ||
| value: string; | ||
| selected: boolean; | ||
| focus: boolean; | ||
| onMouseDown(event: SyntheticEvent): void; | ||
| }; | ||
|
|
||
| type AutoCompleteContactProps = Omit< | ||
| ComponentProps<typeof PaginatedSelectFiltered>, | ||
| 'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem' | 'value' | 'onChange' | ||
| > & { | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| renderItem?: (props: OptionProps, contact: Serialized<ILivechatContactWithManagerData>) => ReactElement; | ||
| }; | ||
|
|
||
| const AutoCompleteContact = ({ value, placeholder, disabled, renderItem, onChange, ...props }: AutoCompleteContactProps): ReactElement => { | ||
| const { t } = useTranslation(); | ||
| const [contactsFilter, setContactFilter] = useState<string>(''); | ||
| const debouncedContactFilter = useDebouncedValue(contactsFilter, 500); | ||
|
|
||
| const { | ||
| data: contactsItems = [], | ||
| fetchNextPage, | ||
| isPending, | ||
| } = useContactsList({ | ||
| filter: debouncedContactFilter, | ||
| }); | ||
|
|
||
| return ( | ||
| <PaginatedSelectFiltered | ||
| {...props} | ||
| aria-busy={isPending} | ||
| placeholder={isPending ? t('Loading...') : placeholder} | ||
| aria-disabled={isPending || disabled} | ||
| disabled={isPending || disabled} | ||
| value={value} | ||
| flexShrink={0} | ||
| filter={contactsFilter} | ||
| setFilter={setContactFilter as (value: string | number | undefined) => void} | ||
| options={contactsItems} | ||
| onChange={onChange} | ||
| endReached={() => fetchNextPage()} | ||
| renderItem={renderItem ? (props: OptionProps) => renderItem(props, contactsItems[props.index]) : undefined} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default memo(AutoCompleteContact); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './AutoCompleteContact'; |
52 changes: 52 additions & 0 deletions
52
apps/meteor/client/components/AutoCompleteContact/useContactsList.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| import type { Serialized } from '@rocket.chat/core-typings'; | ||
| import type { ILivechatContactWithManagerData } from '@rocket.chat/rest-typings'; | ||
| import { useEndpoint } from '@rocket.chat/ui-contexts'; | ||
| import { useInfiniteQuery } from '@tanstack/react-query'; | ||
|
|
||
| import { omnichannelQueryKeys } from '../../lib/queryKeys'; | ||
|
|
||
| export type ContactOption = Serialized<ILivechatContactWithManagerData> & { | ||
| value: string; | ||
| label: string; | ||
| }; | ||
|
|
||
| type ContactOptions = { | ||
| filter: string; | ||
| limit?: number; | ||
| }; | ||
|
|
||
| const DEFAULT_QUERY_LIMIT = 25; | ||
|
|
||
| const formatContactItem = (contact: Serialized<ILivechatContactWithManagerData>): ContactOption => ({ | ||
| ...contact, | ||
| label: contact.name || contact._id, | ||
| value: contact._id, | ||
| }); | ||
|
|
||
| export const useContactsList = (options: ContactOptions) => { | ||
| const getContacts = useEndpoint('GET', '/v1/omnichannel/contacts.search'); | ||
| const { filter, limit = DEFAULT_QUERY_LIMIT } = options; | ||
|
|
||
| return useInfiniteQuery({ | ||
| queryKey: omnichannelQueryKeys.contacts({ filter, limit }), | ||
| queryFn: async ({ pageParam: offset = 0 }) => { | ||
| const { contacts, ...data } = await getContacts({ | ||
| searchText: filter, | ||
| // sort: `{ name: -1 }`, | ||
| ...(limit && { count: limit }), | ||
| ...(offset && { offset }), | ||
| }); | ||
|
|
||
| return { | ||
| ...data, | ||
| contacts: contacts.map(formatContactItem), | ||
| }; | ||
| }, | ||
| select: (data) => data.pages.flatMap<ContactOption>((page) => page.contacts), | ||
| initialPageParam: 0, | ||
| getNextPageParam: (lastPage) => { | ||
| const offset = lastPage.offset + lastPage.count; | ||
| return offset < lastPage.total ? offset : undefined; | ||
| }, | ||
| }); | ||
| }; |
13 changes: 13 additions & 0 deletions
13
...nt/components/Omnichannel/OutboundMessage/components/AutoCompleteDepartmentAgent.spec.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { render, screen } from '@testing-library/react'; | ||
|
|
||
| import AutoCompleteDepartmentAgent from './AutoCompleteDepartmentAgent'; | ||
|
|
||
| it('should not display the placeholder when there is a value', () => { | ||
| const { rerender } = render(<AutoCompleteDepartmentAgent value='' onChange={jest.fn()} placeholder='Select an agent' agents={[]} />); | ||
|
|
||
| expect(screen.getByPlaceholderText('Select an agent')).toBeInTheDocument(); | ||
|
|
||
| rerender(<AutoCompleteDepartmentAgent value='agent1' onChange={jest.fn()} placeholder='Select an agent' agents={[]} />); | ||
|
|
||
| expect(screen.queryByPlaceholderText('Select an agent')).not.toBeInTheDocument(); | ||
| }); |
64 changes: 64 additions & 0 deletions
64
.../client/components/Omnichannel/OutboundMessage/components/AutoCompleteDepartmentAgent.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import type { ILivechatDepartmentAgents, Serialized } from '@rocket.chat/core-typings'; | ||
| import { AutoComplete, Box, Chip, Option, OptionAvatar, OptionContent } from '@rocket.chat/fuselage'; | ||
| import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; | ||
| import { UserAvatar } from '@rocket.chat/ui-avatar'; | ||
| import type { AllHTMLAttributes, ReactElement } from 'react'; | ||
| import { useMemo, useState } from 'react'; | ||
|
|
||
| type AutoCompleteDepartmentAgentProps = Omit<AllHTMLAttributes<HTMLInputElement>, 'onChange'> & { | ||
| error?: boolean; | ||
| value: string; | ||
| onChange(value: string): void; | ||
| agents?: Serialized<ILivechatDepartmentAgents>[]; | ||
| }; | ||
|
|
||
| const AutoCompleteDepartmentAgent = ({ value, onChange, agents, placeholder, ...props }: AutoCompleteDepartmentAgentProps) => { | ||
| const [filter, setFilter] = useState(''); | ||
| const debouncedFilter = useDebouncedValue(filter, 1000); | ||
|
|
||
| const options = useMemo(() => { | ||
| if (!agents) { | ||
| return []; | ||
| } | ||
|
|
||
| return agents | ||
| .filter((agent) => agent.username?.includes(debouncedFilter)) | ||
| .sort((a, b) => a.username.localeCompare(b.username)) | ||
| .map((agent) => ({ | ||
| value: agent.agentId, | ||
| label: agent.username, | ||
| })); | ||
| }, [agents, debouncedFilter]); | ||
|
|
||
| return ( | ||
| <AutoComplete | ||
| {...props} | ||
| placeholder={!value ? placeholder : undefined} | ||
| filter={filter} | ||
| setFilter={setFilter} | ||
| value={value} | ||
| onChange={onChange as (value: string | string[]) => void} | ||
| options={options} | ||
| renderSelected={({ selected: { value, label }, ...props }): ReactElement => { | ||
| return ( | ||
| <Chip {...props} height='x20' value={value} onClick={() => onChange('')} mie={4}> | ||
| <UserAvatar size='x20' username={label} /> | ||
| <Box is='span' margin='none' mis={4}> | ||
| {label} | ||
| </Box> | ||
| </Chip> | ||
| ); | ||
| }} | ||
| renderItem={({ value, label, ...props }): ReactElement => ( | ||
| <Option key={value} {...props}> | ||
| <OptionAvatar> | ||
| <UserAvatar username={label} size='x20' /> | ||
| </OptionAvatar> | ||
| <OptionContent>{label}</OptionContent> | ||
| </Option> | ||
| )} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default AutoCompleteDepartmentAgent; |
69 changes: 69 additions & 0 deletions
69
...client/components/Omnichannel/OutboundMessage/components/AutoCompleteOutboundProvider.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import type { ILivechatContact, Serialized } from '@rocket.chat/core-typings'; | ||
| import { Option, OptionDescription, PaginatedSelectFiltered } from '@rocket.chat/fuselage'; | ||
| import type { ComponentProps, ReactElement } from 'react'; | ||
| import { useState } from 'react'; | ||
| import { useTranslation } from 'react-i18next'; | ||
|
|
||
| import { useTimeFromNow } from '../../../../hooks/useTimeFromNow'; | ||
| import useOutboundProvidersList from '../hooks/useOutboundProvidersList'; | ||
| import { findLastChatFromChannel } from '../utils/findLastChatFromChannel'; | ||
|
|
||
| type AutoCompleteOutboundProviderProps = Omit< | ||
| ComponentProps<typeof PaginatedSelectFiltered>, | ||
| 'filter' | 'setFilter' | 'options' | 'endReached' | 'renderItem' | ||
| > & { | ||
| contact?: Serialized<Omit<ILivechatContact, 'contactManager'>> | null; | ||
| value: string; | ||
| onChange: (value: string) => void; | ||
| }; | ||
|
|
||
| const AutoCompleteOutboundProvider = ({ | ||
| contact, | ||
| disabled, | ||
| value, | ||
| placeholder, | ||
| onChange, | ||
| ...props | ||
| }: AutoCompleteOutboundProviderProps): ReactElement => { | ||
| const [channelsFilter, setChannelsFilter] = useState<string>(''); | ||
| const { t } = useTranslation(); | ||
| const getTimeFromNow = useTimeFromNow(true); | ||
|
|
||
| const { data: options = [], isPending } = useOutboundProvidersList({ | ||
| select: ({ providers = [] }) => { | ||
| return providers.map((prov) => ({ | ||
| label: prov.providerName, | ||
| value: prov.providerId, | ||
| })); | ||
| }, | ||
| }); | ||
|
|
||
| return ( | ||
| <PaginatedSelectFiltered | ||
| {...props} | ||
| aria-busy={isPending} | ||
| placeholder={isPending ? t('Loading...') : placeholder} | ||
| aria-disabled={isPending || disabled} | ||
| disabled={isPending || disabled} | ||
| value={value} | ||
| flexShrink={0} | ||
| filter={channelsFilter} | ||
| setFilter={setChannelsFilter as (value: string | number | undefined) => void} | ||
| options={options} | ||
| onChange={onChange} | ||
| renderItem={({ label, value, ...props }) => { | ||
| const lastChat = findLastChatFromChannel(contact?.channels, value); | ||
|
|
||
| return ( | ||
| <Option {...props} label={label} value={value}> | ||
| {lastChat ? ( | ||
| <OptionDescription>{t('Last_message_received__time__', { time: getTimeFromNow(lastChat) })}</OptionDescription> | ||
| ) : null} | ||
| </Option> | ||
| ); | ||
| }} | ||
| /> | ||
| ); | ||
| }; | ||
|
|
||
| export default AutoCompleteOutboundProvider; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.