diff --git a/packages/ui-inbox/src/settings/integrations/components/mail/MailForm.tsx b/packages/ui-inbox/src/settings/integrations/components/mail/MailForm.tsx index 04e959ec15..f355bdd824 100644 --- a/packages/ui-inbox/src/settings/integrations/components/mail/MailForm.tsx +++ b/packages/ui-inbox/src/settings/integrations/components/mail/MailForm.tsx @@ -40,6 +40,13 @@ import Tip from '@erxes/ui/src/components/Tip'; import Uploader from '@erxes/ui/src/components/Uploader'; import dayjs from 'dayjs'; import { generateEmailTemplateParams } from '@erxes/ui-engage/src/utils'; +import { + MailColumn, + MailSuggestionContainer, + MailSuggestionItem +} from '../../styles'; +import { Avatar } from '@erxes/ui/src/components/SelectWithSearch'; +import { IContact } from '../../types'; type Props = { emailTemplates: any[] /*change type*/; @@ -53,6 +60,7 @@ type Props = { replyAll?: boolean; brandId?: string; mails?: IMessage[]; + contacts?: IContact[]; messageId?: string; totalCount?: number; closeModal?: () => void; @@ -99,12 +107,13 @@ type State = { name: string; showReply: string; isRepliesRetrieved: boolean; + selectedSuggestionIndex: number; + focusInput: string; }; class MailForm extends React.Component { constructor(props: Props) { super(props); - const { isForward, replyAll, mailData = {} as IMail, emailTo } = props; const mailWidget = JSON.parse(localStorage.getItem('emailWidgetData')); @@ -176,7 +185,9 @@ class MailForm extends React.Component { name: `mail_${mailKey}`, showReply: `reply_${mailKey}`, - isRepliesRetrieved: false + isRepliesRetrieved: false, + selectedSuggestionIndex: 0, + focusInput: '' }; } @@ -449,10 +460,9 @@ class MailForm extends React.Component { }; onSelectChange = (name: T, e: any) => { - this.setState(({ [name]: e.currentTarget.value } as unknown) as Pick< - State, - keyof State - >); + this.setState(({ + [name]: e.currentTarget.value + } as unknown) as Pick); this.prepareData(); }; @@ -551,17 +561,155 @@ class MailForm extends React.Component { ); } + getFilteredContacts = (fieldName: string) => { + const field = this.state[fieldName]; + const { contacts } = this.props; + + if ( + field.trim() === '' || + field.substr(field.lastIndexOf(',') + 1).trim() === '' + ) { + return []; + } + + const filterLowerCase = field + .toLowerCase() + .substr(field.lastIndexOf(',') + 1) + .trim(); + + return (contacts || []) + .filter(contact => + [contact.primaryEmail, contact.primaryName].some( + prop => + prop?.toLowerCase().includes(filterLowerCase) && + !field?.includes(contact.primaryEmail) + ) + ) + .slice(0, 5); + }; + + handleSuggestionClick = (contact: IContact, fieldName: string) => { + const field = this.state[fieldName]; + const updatedField = field + ? field.replace( + new RegExp(`${field.substr(field.lastIndexOf(',') + 1).trim()}$`), + '' + ) + + contact.primaryEmail + + ', ' + : contact.primaryEmail + ', '; + + this.setState(({ + [fieldName]: updatedField, + focusInput: fieldName + } as unknown) as Pick); + }; + + handleKeyDown = (e: any, fieldName: string) => { + const { selectedSuggestionIndex } = this.state; + const filterContacts = this.getFilteredContacts(fieldName); + + if (e.keyCode === 38 && selectedSuggestionIndex > 0) { + e.preventDefault(); + this.setState({ + selectedSuggestionIndex: selectedSuggestionIndex - 1 + }); + } + + if ( + e.keyCode === 40 && + selectedSuggestionIndex < filterContacts.length - 1 + ) { + e.preventDefault(); + this.setState({ + selectedSuggestionIndex: selectedSuggestionIndex + 1 + }); + } + + if (e.keyCode === 13) { + const selectedItem = filterContacts[selectedSuggestionIndex]; + selectedItem && this.handleSuggestionClick(selectedItem, fieldName); + } + }; + + renderMailSuggestions(fieldName: string) { + const filterContacts = this.getFilteredContacts(fieldName); + + if (filterContacts.length === 0) { + return null; + } + + return ( + + {filterContacts.map((contact, index) => + this.renderMailSuggestionsRow(contact, index, fieldName) + )} + + ); + } + + renderMailSuggestionsRow( + contact: IContact, + index: number, + fieldName: string + ) { + const { selectedSuggestionIndex } = this.state; + const { primaryName, primaryEmail, avatar } = contact; + const field = this.state[fieldName]; + const fieldRegex = new RegExp( + `(${field.substr(field.lastIndexOf(',') + 1).trim()})`, + 'gi' + ); + + return ( + { + e.preventDefault(); + this.handleSuggestionClick(contact, fieldName); + }} + className={`${index === selectedSuggestionIndex ? 'selected' : ''}`} + > + + + {primaryName && ( +

$1' + ) + }} + /> + )} +

{primaryEmail}

+
+
+ ); + } + renderTo() { return ( - + + this.onSelectChange('to', e)} + name="to" + required={true} + autoComplete="off" + onKeyDown={e => { + this.handleKeyDown(e, 'to'); + }} + onFocus={() => this.setState({ focusInput: 'to' })} + onBlur={() => this.setState({ focusInput: '' })} + /> + {this.state.focusInput === 'to' && this.renderMailSuggestions('to')} + {this.renderRightSide()} ); @@ -577,13 +725,20 @@ class MailForm extends React.Component { return ( - + + { + this.handleKeyDown(e, 'cc'); + }} + onFocus={() => this.setState({ focusInput: 'cc' })} + onBlur={() => this.setState({ focusInput: '' })} + /> + {this.state.focusInput === 'cc' && this.renderMailSuggestions('cc')} + ); } @@ -598,13 +753,20 @@ class MailForm extends React.Component { return ( - + + { + this.handleKeyDown(e, 'bcc'); + }} + onFocus={() => this.setState({ focusInput: 'bcc' })} + onBlur={() => this.setState({ focusInput: '' })} + /> + {this.state.focusInput === 'bcc' && this.renderMailSuggestions('bcc')} + ); } diff --git a/packages/ui-inbox/src/settings/integrations/containers/mail/MailForm.tsx b/packages/ui-inbox/src/settings/integrations/containers/mail/MailForm.tsx index 2c71687e81..16461c768b 100644 --- a/packages/ui-inbox/src/settings/integrations/containers/mail/MailForm.tsx +++ b/packages/ui-inbox/src/settings/integrations/containers/mail/MailForm.tsx @@ -18,6 +18,7 @@ import { isEnabled } from '@erxes/ui/src/utils/core'; import queryString from 'query-string'; import withCurrentUser from '@erxes/ui/src/auth/containers/withCurrentUser'; import { withRouter } from 'react-router-dom'; +import { ContactQueryResponse } from '../../types'; type Props = { detailQuery?: any; @@ -52,6 +53,7 @@ type FinalProps = { currentUser: IUser; emailTemplatesQuery: any /*change type*/; emailTemplatesTotalCountQuery: any /*change type*/; + contactsMainQuery: ContactQueryResponse; } & Props; class MailFormContainer extends React.Component< @@ -82,6 +84,7 @@ class MailFormContainer extends React.Component< closeModal, emailTemplatesQuery, emailTemplatesTotalCountQuery, + contactsMainQuery, currentUser, mails, messageId @@ -254,6 +257,35 @@ class MailFormContainer extends React.Component< }); }; + const contacts = !contactsMainQuery.loading + ? [ + ...contactsMainQuery?.customers?.list, + ...contactsMainQuery?.companies?.list, + ...contactsMainQuery?.leads?.list + ] + .filter(contact => { + return contact.primaryEmail !== null; + }) + .map(contact => { + const { + _id, + firstName, + lastName, + primaryName, + primaryEmail, + avatar + } = contact; + + return { + primaryName: + primaryName || `${firstName || ''} ${lastName || ''}`, + primaryEmail: primaryEmail, + avatar: avatar, + id: _id + }; + }) + : []; + const updatedProps = { ...this.props, sendMail, @@ -265,7 +297,8 @@ class MailFormContainer extends React.Component< mails, messageId, verifiedImapEmails: verifiedImapEmails || [], - verifiedEngageEmails: verifiedEngageEmails || [] + verifiedEngageEmails: verifiedEngageEmails || [], + contacts: contacts || [] }; return ; @@ -293,6 +326,19 @@ const WithMailForm = withProps( fetchPolicy: 'cache-first' }), skip: !isEnabled('emailtemplates') + }), + graphql(gql(queries.contacts), { + name: 'contactsMainQuery', + options: ({ queryParams }) => ({ + variables: { + page: 1, + perPage: 20, + leadType: 'lead', + customerType: 'customer' + }, + fetchPolicy: 'cache-first' + }), + skip: !isEnabled('contacts') }) )(withCurrentUser(MailFormContainer)) ); diff --git a/packages/ui-inbox/src/settings/integrations/containers/utils.tsx b/packages/ui-inbox/src/settings/integrations/containers/utils.tsx index 0ebf1a8fde..7346afa8b5 100644 --- a/packages/ui-inbox/src/settings/integrations/containers/utils.tsx +++ b/packages/ui-inbox/src/settings/integrations/containers/utils.tsx @@ -6,7 +6,9 @@ import { queries } from '../graphql'; import sanitizeHtml from 'sanitize-html'; export const formatStr = (emailString?: string) => { - return emailString ? emailString.split(/[ ,]+/) : []; + return emailString + ? emailString.split(/[ , ]+/).filter(email => email !== '') + : []; }; export const formatObj = (emailArray: IEmail[]) => { diff --git a/packages/ui-inbox/src/settings/integrations/graphql/queries.ts b/packages/ui-inbox/src/settings/integrations/graphql/queries.ts index c9a186ca3e..767bcc77ef 100644 --- a/packages/ui-inbox/src/settings/integrations/graphql/queries.ts +++ b/packages/ui-inbox/src/settings/integrations/graphql/queries.ts @@ -220,6 +220,39 @@ const imapIntegrations = ` } `; +const contacts = isEnabled('contacts') + ? ` + query contactsMain($page: Int, $perPage: Int, $customerType: String, $leadType: String) { + leads: customersMain(page: $page, perPage: $perPage, type: $leadType) { + list { + _id + primaryEmail + firstName + lastName + avatar + } + } + customers: customersMain(page: $page, perPage: $perPage, type: $customerType) { + list { + _id + primaryEmail + firstName + lastName + avatar + } + } + companies : companiesMain(page: $page, perPage: $perPage) { + list { + _id + primaryName + primaryEmail + avatar + } + } + } + ` + : ``; + export default { users, brands, @@ -237,5 +270,6 @@ export default { integrationsGetTwitterAccount, integrationsGetFbPages, integrationsVideoCallUsageStatus, - imapIntegrations + imapIntegrations, + contacts }; diff --git a/packages/ui-inbox/src/settings/integrations/styles.ts b/packages/ui-inbox/src/settings/integrations/styles.ts index c487045690..0139256231 100644 --- a/packages/ui-inbox/src/settings/integrations/styles.ts +++ b/packages/ui-inbox/src/settings/integrations/styles.ts @@ -423,6 +423,48 @@ const SearchInput = styledTS<{ isInPopover: boolean }>(styled.div)` } `; +const MailSuggestionContainer = styled.div` + margin-top: 5px; + position: absolute; + top: calc(100% + 5px); + left: 0; + width: 100%; + overflow: hidden; + background-color: white; + border: none; + border-radius: 0.25rem; + box-shadow: 0 5px 15px 1px rgba(0, 0, 0, 0.15); + z-index: 10; + padding: 5px 0; + outline: none; +`; + +const MailSuggestionItem = styled.div` + display: flex; + padding: 8px 14px; + align-items: center; + gap: 5px; + cursor: pointer; + transition: background-color 0.2s ease-in-out; + + p { + margin: 0; + } + + &:hover { + background-color: #f0f0f0; + } + + &.selected { + background-color: #f1f1f1; + } +`; + +const MailColumn = styled.div` + flex: 1; + position: relative; +`; + export { MessengerPreview, IntegrationName, @@ -455,5 +497,8 @@ export { ShowReplyButtonWrapper, ShowReplies, PopoverLinkWrapper, - SearchInput + SearchInput, + MailSuggestionContainer, + MailSuggestionItem, + MailColumn }; diff --git a/packages/ui-inbox/src/settings/integrations/types.ts b/packages/ui-inbox/src/settings/integrations/types.ts index 4a9d41cee6..b1a98c7d6e 100644 --- a/packages/ui-inbox/src/settings/integrations/types.ts +++ b/packages/ui-inbox/src/settings/integrations/types.ts @@ -195,6 +195,21 @@ export interface ISkillData { }>; } +export type IContact = { + _id?: string; + firstName?: string; + lastName?: string; + primaryName?: string; + primaryEmail: string; + avatar: string; +}; + +export type ContactQueryResponse = { + customers: { list: IContact[] }; + companies: { list: IContact[] }; + leads: { list: IContact[] }; +} & QueryResponse; + export interface IMessengerData { botEndpointUrl?: string; botShowInitialMessage?: boolean;