Files
upstream-jitsi-meet/react/features/salesforce/useSalesforceLinkDialog.ts
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

425 lines
14 KiB
TypeScript

import { debounce } from 'lodash-es';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
import { getCurrentConference } from '../base/conference/functions';
import { hideNotification, showNotification } from '../notifications/actions';
import {
NOTIFICATION_TIMEOUT_TYPE,
NOTIFICATION_TYPE,
SALESFORCE_LINK_NOTIFICATION_ID
} from '../notifications/constants';
import {
confirmPendingAccount,
confirmPendingDeal,
getSessionSalesforceData,
linkSession,
rejectPendingAccounts,
rejectPendingDeals,
searchSalesforce,
unlinkSession
} from './functions';
import {
IAccountMatch,
IContactMatch,
ILeadMatch,
IOpportunityMatch,
ISalesforceData,
ISearchResults,
SalesforceObjectType
} from './types';
export const useSalesforceLinkDialog = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
// Redux state
const conference = useSelector(getCurrentConference);
const sessionId = conference?.getMeetingUniqueId();
const { salesforceUrl = '' } = useSelector((state: IReduxState) => state['features/base/config']);
const { jwt = '' } = useSelector((state: IReduxState) => state['features/base/jwt']);
// Component state
const [ salesforceData, setSalesforceData ] = useState<ISalesforceData | null>(null);
const [ searchResults, setSearchResults ] = useState<ISearchResults | null>(null);
const [ searchTerm, setSearchTerm ] = useState('');
const [ isLoading, setIsLoading ] = useState(true);
const [ isSearching, setIsSearching ] = useState(false);
const [ error, setError ] = useState<string | null>(null);
// Loading states for actions
const [ confirmingAccountId, setConfirmingAccountId ] = useState<string | null>(null);
const [ confirmingDealId, setConfirmingDealId ] = useState<string | null>(null);
const [ rejectingAccounts, setRejectingAccounts ] = useState(false);
const [ rejectingDeals, setRejectingDeals ] = useState(false);
const [ linkingId, setLinkingId ] = useState<string | null>(null);
const [ unlinkingId, setUnlinkingId ] = useState<string | null>(null);
/**
* Refreshes the Salesforce data for the current session.
*/
const refreshSalesforceData = useCallback(async () => {
if (!sessionId) {
return;
}
try {
const data = await getSessionSalesforceData(salesforceUrl, jwt, sessionId);
setSalesforceData(data);
setError(null);
} catch (err: any) {
console.error('Failed to fetch Salesforce data:', err);
setError(err?.error || t('dialog.salesforceDataError'));
}
}, [ salesforceUrl, jwt, sessionId, t ]);
/**
* Performs a search in Salesforce.
*/
const performSearch = useCallback(async (query: string) => {
if (query.length < 2) {
setSearchResults(null);
return;
}
setIsSearching(true);
try {
const results = await searchSalesforce(salesforceUrl, jwt, query);
setSearchResults(results);
setError(null);
} catch (err: any) {
console.error('Salesforce search failed:', err);
setError(err?.error || t('dialog.searchResultsError'));
setSearchResults(null);
} finally {
setIsSearching(false);
}
}, [ salesforceUrl, jwt, t ]);
// Debounced search
const debouncedSearch = useMemo(
() => debounce(performSearch, 500),
[ performSearch ]
);
// Track if component is mounted
const isMounted = useRef(true);
useEffect(() => {
isMounted.current = true;
return () => {
isMounted.current = false;
};
}, []);
// Fetch Salesforce data on mount
useEffect(() => {
const fetchData = async () => {
setIsLoading(true);
await refreshSalesforceData();
if (isMounted.current) {
setIsLoading(false);
}
};
fetchData();
}, [ refreshSalesforceData ]);
// Trigger search when search term changes
useEffect(() => {
if (searchTerm.length >= 2) {
debouncedSearch(searchTerm);
} else {
setSearchResults(null);
}
}, [ searchTerm, debouncedSearch ]);
/**
* Handles confirming a pending account.
*/
const handleConfirmAccount = useCallback(async (accountId: string) => {
if (!sessionId) {
return;
}
setConfirmingAccountId(accountId);
try {
const result = await confirmPendingAccount(salesforceUrl, jwt, sessionId, accountId);
if (result.success) {
dispatch(showNotification({
titleKey: 'notify.confirmAccountSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
await refreshSalesforceData();
}
} catch (err: any) {
dispatch(showNotification({
titleKey: 'notify.confirmAccountError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setConfirmingAccountId(null);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
/**
* Handles rejecting all pending accounts.
*/
const handleRejectAllAccounts = useCallback(async () => {
if (!sessionId) {
return;
}
setRejectingAccounts(true);
try {
await rejectPendingAccounts(salesforceUrl, jwt, sessionId);
dispatch(showNotification({
titleKey: 'notify.dismissSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
await refreshSalesforceData();
} catch (err: any) {
dispatch(showNotification({
titleKey: 'notify.dismissError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setRejectingAccounts(false);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
/**
* Handles confirming a pending deal.
*/
const handleConfirmDeal = useCallback(async (opportunityId: string) => {
if (!sessionId) {
return;
}
setConfirmingDealId(opportunityId);
try {
const result = await confirmPendingDeal(salesforceUrl, jwt, sessionId, opportunityId);
if (result.success) {
dispatch(showNotification({
titleKey: 'notify.confirmDealSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
await refreshSalesforceData();
}
} catch (err: any) {
dispatch(showNotification({
titleKey: 'notify.confirmDealError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setConfirmingDealId(null);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
/**
* Handles rejecting all pending deals.
*/
const handleRejectAllDeals = useCallback(async () => {
if (!sessionId) {
return;
}
setRejectingDeals(true);
try {
await rejectPendingDeals(salesforceUrl, jwt, sessionId);
dispatch(showNotification({
titleKey: 'notify.dismissSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
await refreshSalesforceData();
} catch (err: any) {
dispatch(showNotification({
titleKey: 'notify.dismissError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setRejectingDeals(false);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
/**
* Handles linking a record from search results.
*/
const handleLinkRecord = useCallback(async (
type: SalesforceObjectType,
data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch
) => {
if (!sessionId) {
return;
}
const id = type === 'Account' ? (data as IAccountMatch).accountId
: type === 'Lead' ? (data as ILeadMatch).leadId
: type === 'Contact' ? (data as IContactMatch).contactId
: (data as IOpportunityMatch).opportunityId;
setLinkingId(id);
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceProgress',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
try {
await linkSession(salesforceUrl, jwt, sessionId, type, data);
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
// Clear search and refresh data
setSearchTerm('');
setSearchResults(null);
await refreshSalesforceData();
} catch (err: any) {
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setLinkingId(null);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
/**
* Handles unlinking a record from the session.
*/
const handleUnlinkRecord = useCallback(async (type: SalesforceObjectType, id: string) => {
if (!sessionId) {
return;
}
setUnlinkingId(id);
try {
await unlinkSession(salesforceUrl, jwt, sessionId, type, id);
dispatch(showNotification({
titleKey: 'notify.unlinkFromSalesforceSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.SHORT));
await refreshSalesforceData();
} catch (err: any) {
dispatch(showNotification({
titleKey: 'notify.unlinkFromSalesforceError',
descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} finally {
setUnlinkingId(null);
}
}, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
// Filter out already-linked items from search results
const filteredSearchResults = useMemo(() => {
if (!searchResults || !salesforceData) {
return searchResults;
}
return {
accounts: searchResults.accounts.filter(
account => salesforceData.account?.accountId !== account.accountId
),
leads: searchResults.leads.filter(
lead => !salesforceData.leads?.some(l => l.leadId === lead.leadId)
),
contacts: searchResults.contacts.filter(
contact => !salesforceData.contacts?.some(c => c.contactId === contact.contactId)
),
opportunities: searchResults.opportunities.filter(
opp => salesforceData.deal?.opportunityId !== opp.opportunityId
)
};
}, [ searchResults, salesforceData ]);
const hasSearchResults = filteredSearchResults
&& (filteredSearchResults.accounts.length > 0
|| filteredSearchResults.leads.length > 0
|| filteredSearchResults.contacts.length > 0
|| filteredSearchResults.opportunities.length > 0);
const hasPendingAccounts = (salesforceData?.pendingAccounts?.length ?? 0) > 0;
const hasPendingDeals = (salesforceData?.pendingDeals?.length ?? 0) > 0;
return {
// Data
salesforceData,
searchResults: filteredSearchResults,
hasSearchResults,
hasPendingAccounts,
hasPendingDeals,
// State
searchTerm,
isLoading,
isSearching,
error,
// Action loading states
confirmingAccountId,
confirmingDealId,
rejectingAccounts,
rejectingDeals,
linkingId,
unlinkingId,
// Setters
setSearchTerm,
// Actions
handleConfirmAccount,
handleRejectAllAccounts,
handleConfirmDeal,
handleRejectAllDeals,
handleLinkRecord,
handleUnlinkRecord,
refreshSalesforceData
};
};