snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
146
react/features/salesforce/components/web/CurrentLinksSection.tsx
Normal file
146
react/features/salesforce/components/web/CurrentLinksSection.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import {
|
||||
IconRecordAccount,
|
||||
IconRecordContact,
|
||||
IconRecordLead,
|
||||
IconRecordOpportunity
|
||||
} from '../../../base/icons/svg';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { ISalesforceData, SalesforceObjectType } from '../../types';
|
||||
|
||||
import { RecordListItem } from './RecordListItem';
|
||||
|
||||
interface IProps {
|
||||
onUnlink: (type: SalesforceObjectType, id: string) => void;
|
||||
salesforceData: ISalesforceData | null;
|
||||
unlinkingId: string | null;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
section: {
|
||||
marginTop: '24px',
|
||||
paddingTop: '16px',
|
||||
borderTop: `1px solid ${theme.palette.ui05}`
|
||||
},
|
||||
title: {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
margin: '0 0 12px 0'
|
||||
},
|
||||
list: {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying currently linked Salesforce objects with unlink buttons.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React.ReactElement}
|
||||
*/
|
||||
export const CurrentLinksSection = ({ salesforceData, unlinkingId, onUnlink }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const hasAccount = Boolean(salesforceData?.account);
|
||||
const hasLeads = salesforceData?.leads?.length;
|
||||
const hasContacts = salesforceData?.contacts?.length;
|
||||
const hasDeal = Boolean(salesforceData?.deal);
|
||||
const hasAnyLinks = hasAccount || hasLeads || hasContacts || hasDeal;
|
||||
|
||||
const handleUnlinkAccount = useCallback((id: string) => {
|
||||
onUnlink('Account', id);
|
||||
}, [ onUnlink ]);
|
||||
|
||||
const handleUnlinkLead = useCallback((id: string) => {
|
||||
onUnlink('Lead', id);
|
||||
}, [ onUnlink ]);
|
||||
|
||||
const handleUnlinkContact = useCallback((id: string) => {
|
||||
onUnlink('Contact', id);
|
||||
}, [ onUnlink ]);
|
||||
|
||||
const handleUnlinkOpportunity = useCallback((id: string) => {
|
||||
onUnlink('Opportunity', id);
|
||||
}, [ onUnlink ]);
|
||||
|
||||
if (!hasAnyLinks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.section }>
|
||||
<h3 className = { classes.title }>{t('dialog.currentLinks')}</h3>
|
||||
<ul className = { classes.list }>
|
||||
{salesforceData?.account && (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.unlink'
|
||||
actionType = { BUTTON_TYPES.TERTIARY }
|
||||
disabled = { unlinkingId !== null }
|
||||
icon = { IconRecordAccount }
|
||||
isLoading = { unlinkingId === salesforceData.account.accountId }
|
||||
key = { salesforceData.account.accountId }
|
||||
metadata = { t('record.type.account') }
|
||||
name = { salesforceData.account.accountName }
|
||||
onAction = { handleUnlinkAccount }
|
||||
record = { salesforceData.account.accountId } />
|
||||
)}
|
||||
|
||||
{salesforceData?.leads?.map(lead => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.unlink'
|
||||
actionType = { BUTTON_TYPES.TERTIARY }
|
||||
disabled = { unlinkingId !== null }
|
||||
icon = { IconRecordLead }
|
||||
isLoading = { unlinkingId === lead.leadId }
|
||||
key = { lead.leadId }
|
||||
metadata = { `${t('record.type.lead')}${lead.leadCompany ? ` \u2022 ${lead.leadCompany}` : ''}` }
|
||||
name = { lead.leadName }
|
||||
onAction = { handleUnlinkLead }
|
||||
record = { lead.leadId } />
|
||||
))}
|
||||
|
||||
{salesforceData?.contacts?.map(contact => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.unlink'
|
||||
actionType = { BUTTON_TYPES.TERTIARY }
|
||||
disabled = { unlinkingId !== null }
|
||||
icon = { IconRecordContact }
|
||||
isLoading = { unlinkingId === contact.contactId }
|
||||
key = { contact.contactId }
|
||||
metadata = { t('record.type.contact') }
|
||||
name = { contact.contactName }
|
||||
onAction = { handleUnlinkContact }
|
||||
record = { contact.contactId } />
|
||||
))}
|
||||
|
||||
{salesforceData?.deal && (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.unlink'
|
||||
actionType = { BUTTON_TYPES.TERTIARY }
|
||||
disabled = { unlinkingId !== null }
|
||||
icon = { IconRecordOpportunity }
|
||||
isLoading = { unlinkingId === salesforceData.deal.opportunityId }
|
||||
key = { salesforceData.deal.opportunityId }
|
||||
metadata = { `${salesforceData.deal.opportunityStage}${
|
||||
salesforceData.deal.amount !== undefined
|
||||
? ` \u2022 $${salesforceData.deal.amount.toLocaleString()}`
|
||||
: ''
|
||||
}` }
|
||||
name = { salesforceData.deal.opportunityName }
|
||||
onAction = { handleUnlinkOpportunity }
|
||||
record = { salesforceData.deal.opportunityId } />
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentLinksSection;
|
||||
198
react/features/salesforce/components/web/PendingSection.tsx
Normal file
198
react/features/salesforce/components/web/PendingSection.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Spinner from '../../../base/ui/components/web/Spinner';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import { IPendingAccount, IPendingDeal } from '../../types';
|
||||
|
||||
interface IPendingAccountsSectionProps {
|
||||
confirmingId: string | null;
|
||||
items: IPendingAccount[];
|
||||
onConfirm: (accountId: string) => void;
|
||||
onRejectAll: () => void;
|
||||
rejecting: boolean;
|
||||
type: 'accounts';
|
||||
}
|
||||
|
||||
interface IPendingDealsSectionProps {
|
||||
confirmingId: string | null;
|
||||
items: IPendingDeal[];
|
||||
onConfirm: (opportunityId: string) => void;
|
||||
onRejectAll: () => void;
|
||||
rejecting: boolean;
|
||||
type: 'deals';
|
||||
}
|
||||
|
||||
type IProps = IPendingAccountsSectionProps | IPendingDealsSectionProps;
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
section: {
|
||||
marginBottom: '24px'
|
||||
},
|
||||
header: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: '8px'
|
||||
},
|
||||
title: {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 600,
|
||||
margin: 0
|
||||
},
|
||||
description: {
|
||||
fontSize: '0.75rem',
|
||||
color: theme.palette.text03,
|
||||
marginBottom: '12px'
|
||||
},
|
||||
list: {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: '12px',
|
||||
backgroundColor: theme.palette.inputFieldBackground,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: '8px',
|
||||
|
||||
'&:last-child': {
|
||||
marginBottom: 0
|
||||
}
|
||||
},
|
||||
itemInfo: {
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
},
|
||||
itemName: {
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
itemMeta: {
|
||||
fontSize: '0.75rem',
|
||||
color: theme.palette.text03,
|
||||
marginTop: '2px'
|
||||
},
|
||||
buttonContainer: {
|
||||
marginLeft: '12px',
|
||||
flexShrink: 0
|
||||
},
|
||||
spinnerContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '70px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying pending Salesforce suggestions (accounts or deals).
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React.ReactElement}
|
||||
*/
|
||||
export const PendingSection = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const { items, confirmingId, rejecting, onConfirm, onRejectAll, type } = props;
|
||||
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isAccounts = type === 'accounts';
|
||||
const title = isAccounts
|
||||
? t('dialog.suggestedAccounts')
|
||||
: t('dialog.suggestedDeals');
|
||||
const description = isAccounts
|
||||
? t('dialog.pendingDescription')
|
||||
: t('dialog.pendingDealsDescription');
|
||||
|
||||
return (
|
||||
<div className = { classes.section }>
|
||||
<div className = { classes.header }>
|
||||
<h3 className = { classes.title }>{title}</h3>
|
||||
<Button
|
||||
disabled = { rejecting }
|
||||
labelKey = 'dialog.dismissAll'
|
||||
onClick = { onRejectAll }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.TERTIARY } />
|
||||
</div>
|
||||
<p className = { classes.description }>{description}</p>
|
||||
<ul className = { classes.list }>
|
||||
{isAccounts ? (
|
||||
(items as IPendingAccount[]).map(account => (
|
||||
<li
|
||||
className = { classes.item }
|
||||
key = { account.accountId }>
|
||||
<div className = { classes.itemInfo }>
|
||||
<div className = { classes.itemName }>{account.accountName}</div>
|
||||
{account.matchedEmailDomain && (
|
||||
<div className = { classes.itemMeta }>{account.matchedEmailDomain}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className = { classes.buttonContainer }>
|
||||
{confirmingId === account.accountId ? (
|
||||
<div className = { classes.spinnerContainer }>
|
||||
<Spinner size = 'small' />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
disabled = { confirmingId !== null }
|
||||
labelKey = 'dialog.confirm'
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onClick = { () => onConfirm(account.accountId) }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
(items as IPendingDeal[]).map(deal => (
|
||||
<li
|
||||
className = { classes.item }
|
||||
key = { deal.opportunityId }>
|
||||
<div className = { classes.itemInfo }>
|
||||
<div className = { classes.itemName }>{deal.opportunityName}</div>
|
||||
<div className = { classes.itemMeta }>
|
||||
{deal.opportunityStage}
|
||||
{deal.amount !== undefined && ` \u2022 $${deal.amount.toLocaleString()}`}
|
||||
</div>
|
||||
</div>
|
||||
<div className = { classes.buttonContainer }>
|
||||
{confirmingId === deal.opportunityId ? (
|
||||
<div className = { classes.spinnerContainer }>
|
||||
<Spinner size = 'small' />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
disabled = { confirmingId !== null }
|
||||
labelKey = 'dialog.confirm'
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onClick = { () => onConfirm(deal.opportunityId) }
|
||||
size = 'small'
|
||||
type = { BUTTON_TYPES.PRIMARY } />
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PendingSection;
|
||||
160
react/features/salesforce/components/web/RecordListItem.tsx
Normal file
160
react/features/salesforce/components/web/RecordListItem.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import Button from '../../../base/ui/components/web/Button';
|
||||
import Spinner from '../../../base/ui/components/web/Spinner';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
|
||||
interface IProps<T> {
|
||||
|
||||
/**
|
||||
* Translation key for the action button.
|
||||
*/
|
||||
actionLabelKey: string;
|
||||
|
||||
/**
|
||||
* Button type for the action button.
|
||||
*/
|
||||
actionType?: BUTTON_TYPES;
|
||||
|
||||
/**
|
||||
* Whether the action button should be disabled.
|
||||
*/
|
||||
disabled?: boolean;
|
||||
|
||||
/**
|
||||
* The icon component to display.
|
||||
*/
|
||||
icon: React.ComponentType;
|
||||
|
||||
/**
|
||||
* Whether this item's action is in progress.
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
|
||||
/**
|
||||
* Secondary text (type label, company, stage, etc.).
|
||||
*/
|
||||
metadata?: string;
|
||||
|
||||
/**
|
||||
* The primary display name.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Click handler for the action button.
|
||||
*/
|
||||
onAction: (record: T) => void;
|
||||
|
||||
/**
|
||||
* The record data to pass back when action is triggered.
|
||||
*/
|
||||
record: T;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
item: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px 12px',
|
||||
backgroundColor: theme.palette.ui03,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
marginBottom: '6px',
|
||||
|
||||
'&:last-child': {
|
||||
marginBottom: 0
|
||||
}
|
||||
},
|
||||
iconContainer: {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
marginRight: '12px',
|
||||
flexShrink: 0,
|
||||
|
||||
'& svg': {
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: theme.shape.borderRadius
|
||||
}
|
||||
},
|
||||
itemInfo: {
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
},
|
||||
itemName: {
|
||||
fontSize: '0.875rem',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
},
|
||||
itemMeta: {
|
||||
fontSize: '0.75rem',
|
||||
color: theme.palette.text03,
|
||||
marginTop: '2px'
|
||||
},
|
||||
buttonContainer: {
|
||||
marginLeft: '12px',
|
||||
flexShrink: 0
|
||||
},
|
||||
spinnerContainer: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '70px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Reusable list item component for Salesforce records with an action button.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React.ReactElement}
|
||||
*/
|
||||
export const RecordListItem = <T, >({
|
||||
actionLabelKey,
|
||||
actionType = BUTTON_TYPES.PRIMARY,
|
||||
disabled = false,
|
||||
icon: Icon,
|
||||
isLoading = false,
|
||||
metadata,
|
||||
name,
|
||||
onAction,
|
||||
record
|
||||
}: IProps<T>) => {
|
||||
const { classes } = useStyles();
|
||||
|
||||
const handleAction = useCallback(() => {
|
||||
onAction(record);
|
||||
}, [ onAction, record ]);
|
||||
|
||||
return (
|
||||
<div className = { classes.item }>
|
||||
<div className = { classes.iconContainer }>
|
||||
<Icon />
|
||||
</div>
|
||||
<div className = { classes.itemInfo }>
|
||||
<div className = { classes.itemName }>{name}</div>
|
||||
{metadata && <div className = { classes.itemMeta }>{metadata}</div>}
|
||||
</div>
|
||||
<div className = { classes.buttonContainer }>
|
||||
{isLoading ? (
|
||||
<div className = { classes.spinnerContainer }>
|
||||
<Spinner size = 'small' />
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
disabled = { disabled }
|
||||
labelKey = { actionLabelKey }
|
||||
onClick = { handleAction }
|
||||
size = 'small'
|
||||
type = { actionType } />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordListItem;
|
||||
@@ -0,0 +1,218 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { hideDialog } from '../../../base/dialog/actions';
|
||||
import Icon from '../../../base/icons/components/Icon';
|
||||
import { IconSearch } from '../../../base/icons/svg';
|
||||
import { getFieldValue } from '../../../base/react/functions';
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import Spinner from '../../../base/ui/components/web/Spinner';
|
||||
import { useSalesforceLinkDialog } from '../../useSalesforceLinkDialog';
|
||||
|
||||
import { CurrentLinksSection } from './CurrentLinksSection';
|
||||
import { PendingSection } from './PendingSection';
|
||||
import { SearchResultsSection } from './SearchResultsSection';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
minHeight: '400px',
|
||||
maxHeight: '500px',
|
||||
overflowY: 'auto',
|
||||
position: 'relative'
|
||||
},
|
||||
searchContainer: {
|
||||
position: 'relative',
|
||||
padding: '1px',
|
||||
marginBottom: '16px'
|
||||
},
|
||||
searchIcon: {
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
color: theme.palette.salesforceSearchIcon,
|
||||
left: 16,
|
||||
top: 10,
|
||||
width: 20,
|
||||
height: 20
|
||||
},
|
||||
searchInput: {
|
||||
backgroundColor: theme.palette.salesforceSearchBackground,
|
||||
border: '1px solid',
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
borderColor: theme.palette.salesforceSearchBorder,
|
||||
color: theme.palette.salesforceSearchText,
|
||||
padding: '10px 16px 10px 44px',
|
||||
width: '100%',
|
||||
height: 40,
|
||||
'&::placeholder': {
|
||||
color: theme.palette.salesforceSearchPlaceholder,
|
||||
...theme.typography.bodyShortRegular
|
||||
}
|
||||
},
|
||||
spinner: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
padding: '48px 0',
|
||||
boxSizing: 'border-box'
|
||||
},
|
||||
error: {
|
||||
textAlign: 'center',
|
||||
color: theme.palette.textError,
|
||||
padding: '24px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Component that renders the Salesforce link dialog.
|
||||
*
|
||||
* @returns {React$Element<any>}
|
||||
*/
|
||||
function SalesforceLinkDialog() {
|
||||
const { t } = useTranslation();
|
||||
const { classes, theme } = useStyles();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
salesforceData,
|
||||
searchResults,
|
||||
hasSearchResults,
|
||||
hasPendingAccounts,
|
||||
hasPendingDeals,
|
||||
searchTerm,
|
||||
isLoading,
|
||||
isSearching,
|
||||
error,
|
||||
confirmingAccountId,
|
||||
confirmingDealId,
|
||||
rejectingAccounts,
|
||||
rejectingDeals,
|
||||
linkingId,
|
||||
unlinkingId,
|
||||
setSearchTerm,
|
||||
handleConfirmAccount,
|
||||
handleRejectAllAccounts,
|
||||
handleConfirmDeal,
|
||||
handleRejectAllDeals,
|
||||
handleLinkRecord,
|
||||
handleUnlinkRecord
|
||||
} = useSalesforceLinkDialog();
|
||||
|
||||
const handleSearchChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = getFieldValue(event);
|
||||
|
||||
setSearchTerm(value);
|
||||
}, [ setSearchTerm ]);
|
||||
|
||||
const handleSearchKeyDown = useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
dispatch(hideDialog());
|
||||
}, [ dispatch ]);
|
||||
|
||||
const renderSearch = () => (
|
||||
<div className = { classes.searchContainer }>
|
||||
<Icon
|
||||
className = { classes.searchIcon }
|
||||
color = { theme.palette.icon03 }
|
||||
src = { IconSearch } />
|
||||
<input
|
||||
autoComplete = 'off'
|
||||
autoFocus = { true }
|
||||
className = { classes.searchInput }
|
||||
name = 'salesforceSearch'
|
||||
onChange = { handleSearchChange }
|
||||
onKeyDown = { handleSearchKeyDown }
|
||||
placeholder = { t('dialog.searchInSalesforce') }
|
||||
tabIndex = { 0 }
|
||||
value = { searchTerm } />
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className = { classes.spinner }>
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Search spinner */}
|
||||
{isSearching && (
|
||||
<div className = { classes.spinner }>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search results */}
|
||||
{!isSearching && hasSearchResults && searchResults && (
|
||||
<SearchResultsSection
|
||||
linkingId = { linkingId }
|
||||
onLink = { handleLinkRecord }
|
||||
results = { searchResults } />
|
||||
)}
|
||||
|
||||
{/* Error (only show when not searching and no results) */}
|
||||
{!isSearching && !hasSearchResults && error && (
|
||||
<div className = { classes.error }>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Pending suggestions (when not searching) */}
|
||||
{!searchTerm && hasPendingAccounts && salesforceData?.pendingAccounts && (
|
||||
<PendingSection
|
||||
confirmingId = { confirmingAccountId }
|
||||
items = { salesforceData.pendingAccounts }
|
||||
onConfirm = { handleConfirmAccount }
|
||||
onRejectAll = { handleRejectAllAccounts }
|
||||
rejecting = { rejectingAccounts }
|
||||
type = 'accounts' />
|
||||
)}
|
||||
|
||||
{!searchTerm && hasPendingDeals && salesforceData?.pendingDeals && (
|
||||
<PendingSection
|
||||
confirmingId = { confirmingDealId }
|
||||
items = { salesforceData.pendingDeals }
|
||||
onConfirm = { handleConfirmDeal }
|
||||
onRejectAll = { handleRejectAllDeals }
|
||||
rejecting = { rejectingDeals }
|
||||
type = 'deals' />
|
||||
)}
|
||||
|
||||
{/* Current links */}
|
||||
<CurrentLinksSection
|
||||
onUnlink = { handleUnlinkRecord }
|
||||
salesforceData = { salesforceData }
|
||||
unlinkingId = { unlinkingId } />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
disableEnter = { true }
|
||||
ok = {{ hidden: true }}
|
||||
onCancel = { handleClose }
|
||||
titleKey = 'dialog.linkMeetingTitle'>
|
||||
<div className = { classes.container }>
|
||||
{renderSearch()}
|
||||
{renderContent()}
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default SalesforceLinkDialog;
|
||||
@@ -0,0 +1,183 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import {
|
||||
IconRecordAccount,
|
||||
IconRecordContact,
|
||||
IconRecordLead,
|
||||
IconRecordOpportunity
|
||||
} from '../../../base/icons/svg';
|
||||
import { BUTTON_TYPES } from '../../../base/ui/constants.web';
|
||||
import {
|
||||
IAccountMatch,
|
||||
IContactMatch,
|
||||
ILeadMatch,
|
||||
IOpportunityMatch,
|
||||
ISearchResults,
|
||||
SalesforceObjectType
|
||||
} from '../../types';
|
||||
|
||||
import { RecordListItem } from './RecordListItem';
|
||||
|
||||
interface IProps {
|
||||
linkingId: string | null;
|
||||
onLink: (type: SalesforceObjectType, data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch) => void;
|
||||
results: ISearchResults;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
section: {
|
||||
marginBottom: '16px'
|
||||
},
|
||||
groupTitle: {
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 600,
|
||||
color: theme.palette.text03,
|
||||
textTransform: 'uppercase',
|
||||
margin: '16px 0 8px 0'
|
||||
},
|
||||
list: {
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0
|
||||
},
|
||||
noResults: {
|
||||
textAlign: 'center',
|
||||
color: theme.palette.text03,
|
||||
padding: '24px'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Component for displaying Salesforce search results with link buttons.
|
||||
*
|
||||
* @param {IProps} props - Component props.
|
||||
* @returns {React.ReactElement}
|
||||
*/
|
||||
export const SearchResultsSection = ({ results, linkingId, onLink }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const hasResults = results.accounts.length
|
||||
|| results.leads.length
|
||||
|| results.contacts.length
|
||||
|| results.opportunities.length;
|
||||
|
||||
const handleLinkAccount = useCallback((account: IAccountMatch) => {
|
||||
onLink('Account', account);
|
||||
}, [ onLink ]);
|
||||
|
||||
const handleLinkLead = useCallback((lead: ILeadMatch) => {
|
||||
onLink('Lead', lead);
|
||||
}, [ onLink ]);
|
||||
|
||||
const handleLinkContact = useCallback((contact: IContactMatch) => {
|
||||
onLink('Contact', contact);
|
||||
}, [ onLink ]);
|
||||
|
||||
const handleLinkOpportunity = useCallback((opportunity: IOpportunityMatch) => {
|
||||
onLink('Opportunity', opportunity);
|
||||
}, [ onLink ]);
|
||||
|
||||
if (!hasResults) {
|
||||
return (
|
||||
<div className = { classes.noResults }>
|
||||
{t('dialog.searchResultsNotFound')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { classes.section }>
|
||||
{!!results.accounts.length && (
|
||||
<>
|
||||
<h4 className = { classes.groupTitle }>{t('record.type.account')}</h4>
|
||||
<ul className = { classes.list }>
|
||||
{results.accounts.map(account => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.link'
|
||||
actionType = { BUTTON_TYPES.PRIMARY }
|
||||
disabled = { linkingId !== null }
|
||||
icon = { IconRecordAccount }
|
||||
isLoading = { linkingId === account.accountId }
|
||||
key = { account.accountId }
|
||||
name = { account.accountName }
|
||||
onAction = { handleLinkAccount }
|
||||
record = { account } />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!results.leads.length && (
|
||||
<>
|
||||
<h4 className = { classes.groupTitle }>{t('record.type.lead')}</h4>
|
||||
<ul className = { classes.list }>
|
||||
{results.leads.map(lead => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.link'
|
||||
actionType = { BUTTON_TYPES.PRIMARY }
|
||||
disabled = { linkingId !== null }
|
||||
icon = { IconRecordLead }
|
||||
isLoading = { linkingId === lead.leadId }
|
||||
key = { lead.leadId }
|
||||
metadata = { lead.leadCompany }
|
||||
name = { lead.leadName }
|
||||
onAction = { handleLinkLead }
|
||||
record = { lead } />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!results.contacts.length && (
|
||||
<>
|
||||
<h4 className = { classes.groupTitle }>{t('record.type.contact')}</h4>
|
||||
<ul className = { classes.list }>
|
||||
{results.contacts.map(contact => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.link'
|
||||
actionType = { BUTTON_TYPES.PRIMARY }
|
||||
disabled = { linkingId !== null }
|
||||
icon = { IconRecordContact }
|
||||
isLoading = { linkingId === contact.contactId }
|
||||
key = { contact.contactId }
|
||||
metadata = { contact.contactEmail }
|
||||
name = { contact.contactName }
|
||||
onAction = { handleLinkContact }
|
||||
record = { contact } />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!!results.opportunities.length && (
|
||||
<>
|
||||
<h4 className = { classes.groupTitle }>{t('record.type.opportunity')}</h4>
|
||||
<ul className = { classes.list }>
|
||||
{results.opportunities.map(opp => (
|
||||
<RecordListItem
|
||||
actionLabelKey = 'dialog.link'
|
||||
actionType = { BUTTON_TYPES.PRIMARY }
|
||||
disabled = { linkingId !== null }
|
||||
icon = { IconRecordOpportunity }
|
||||
isLoading = { linkingId === opp.opportunityId }
|
||||
key = { opp.opportunityId }
|
||||
metadata = { `${opp.opportunityStage}${
|
||||
opp.amount !== undefined ? ` \u2022 $${opp.amount.toLocaleString()}` : ''
|
||||
}` }
|
||||
name = { opp.opportunityName }
|
||||
onAction = { handleLinkOpportunity }
|
||||
record = { opp } />
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResultsSection;
|
||||
Reference in New Issue
Block a user