Compare commits

...

1 Commits

Author SHA1 Message Date
damencho
5b4b0107f5 fix: Drops salesforce integration. 2025-07-23 17:24:12 +03:00
26 changed files with 0 additions and 1355 deletions

View File

@@ -866,7 +866,6 @@ var config = {
// 'help',
// 'highlight',
// 'invite',
// 'linktosalesforce',
// 'livestreaming',
// 'microphone',
// 'noisesuppression',
@@ -1390,13 +1389,6 @@ var config = {
logoClickUrl: 'https://example-company.org',
// The url used for the image used as logo
logoImageUrl: 'https://example.com/logo-img.png',
// Endpoint that enables support for salesforce integration with in-meeting resource linking
// This is required for:
// listing the most recent records - salesforceUrl/records/recents
// searching records - salesforceUrl/records?text=${text}
// retrieving record details - salesforceUrl/records/${id}?type=${type}
// and linking the meeting - salesforceUrl/sessions/${sessionId}/records/${id}
// salesforceUrl: 'https://api.example.com/',
// Overwrite for pool of background images for avatars
avatarBackgrounds: ['url(https://example.com/avatar-background-1.png)', '#FFF'],
// The lobby/prejoin screen background
@@ -1713,7 +1705,6 @@ var config = {
// 'notify.leftOneMember', // show when a participant left
// 'notify.leftThreePlusMembers', // show when more than 2 participants left simultaneously
// 'notify.leftTwoMembers', // show when two participants left simultaneously
// 'notify.linkToSalesforce', // shown when joining a meeting with salesforce integration
// 'notify.localRecordingStarted', // shown when the local recording has been started
// 'notify.localRecordingStopped', // shown when the local recording has been stopped
// 'notify.moderationInEffectCSTitle', // shown when user attempts to share content during AV moderation

View File

@@ -291,8 +291,6 @@
"sharingTabs": "Sharing options"
},
"add": "Add",
"addMeetingNote": "Add a note about this meeting",
"addOptionalNote": "Add a note (optional):",
"allow": "Allow",
"allowToggleCameraDialog": "Do you allow {{initiatorName}} to toggle your camera facing mode?",
"allowToggleCameraTitle": "Allow toggle camera?",
@@ -353,8 +351,6 @@
"kickSystemTitle": "Ouch! You were kicked out of the meeting",
"kickTitle": "Ouch! {{participantDisplayName}} kicked you out of the meeting",
"learnMore": "learn more",
"linkMeeting": "Link meeting",
"linkMeetingTitle": "Link meeting to Salesforce",
"liveStreaming": "Live Streaming",
"liveStreamingDisabledBecauseOfActiveRecordingTooltip": "Not possible while recording is active",
"localUserControls": "Local user controls",
@@ -447,12 +443,6 @@
"screenSharingFailed": "Oops! Something went wrong, we weren't able to start screen sharing!",
"screenSharingFailedTitle": "Screen sharing failed!",
"screenSharingPermissionDeniedError": "Oops! Something went wrong with your screen sharing permissions. Please reload and try again.",
"searchInSalesforce": "Search in Salesforce",
"searchResults": "Search results({{count}})",
"searchResultsDetailsError": "Something went wrong while retrieving owner data.",
"searchResultsError": "Something went wrong while retrieving data.",
"searchResultsNotFound": "No search results found.",
"searchResultsTryAgain": "Try using alternative keywords.",
"sendPrivateMessage": "You recently received a private message. Did you intend to reply to that privately, or you want to send your message to the group?",
"sendPrivateMessageCancel": "Send to the group",
"sendPrivateMessageOk": "Send privately",
@@ -818,12 +808,6 @@
"leftOneMember": "{{name}} left the meeting",
"leftThreePlusMembers": "{{name}} and many others left the meeting",
"leftTwoMembers": "{{first}} and {{second}} left the meeting",
"linkToSalesforce": "Link to Salesforce",
"linkToSalesforceDescription": "You can link the meeting summary to a Salesforce object.",
"linkToSalesforceError": "Failed to link meeting to Salesforce",
"linkToSalesforceKey": "Link this meeting",
"linkToSalesforceProgress": "Linking meeting to Salesforce…",
"linkToSalesforceSuccess": "The meeting was linked to Salesforce",
"localRecordingStarted": "{{name}} has started a local recording.",
"localRecordingStopped": "{{name}} has stopped a local recording.",
"me": "Me",
@@ -1066,18 +1050,6 @@
},
"raisedHand": "Would like to speak",
"raisedHandsLabel": "Number of raised hands",
"record": {
"already": {
"linked": "The meeting is already linked to this Salesforce object."
},
"type": {
"account": "Account",
"contact": "Contact",
"lead": "Lead",
"opportunity": "Opportunity",
"owner": "Owner"
}
},
"recording": {
"authDropboxText": "Upload to Dropbox",
"availableSpace": "Available space: {{spaceLeft}} MB (approximately {{duration}} minutes of recording)",
@@ -1314,7 +1286,6 @@
"laugh": "Laugh",
"leaveConference": "Leave meeting",
"like": "Thumbs Up",
"linkToSalesforce": "Link to Salesforce",
"lobbyButton": "Enable/disable lobby mode",
"localRecording": "Toggle local recording controls",
"lockRoom": "Toggle meeting password",
@@ -1404,7 +1375,6 @@
"leaveBreakoutRoom": "Leave breakout room",
"leaveConference": "Leave meeting",
"like": "Thumbs Up",
"linkToSalesforce": "Link to Salesforce",
"lobbyButtonDisable": "Disable lobby mode",
"lobbyButtonEnable": "Enable lobby mode",
"login": "Log-in",

View File

@@ -560,7 +560,6 @@ export interface IConfig {
requireDisplayName?: boolean;
resolution?: number;
roomPasswordNumberOfDigits?: number;
salesforceUrl?: string;
screenshotCapture?: {
enabled?: boolean;
mode?: 'always' | 'recording';

View File

@@ -127,7 +127,6 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
etherpadBase,
liveStreamingDialogUrls = {},
preCallTest = {},
salesforceUrl,
userDocumentationUrl,
peopleSearchUrl,
} = action.value;
@@ -161,10 +160,6 @@ function _setDynamicBrandingData({ dispatch }: IStore, next: Function, action: A
}
}
if (salesforceUrl) {
config.salesforceUrl = salesforceUrl;
}
if (peopleSearchUrl) {
config.peopleSearchUrl = peopleSearchUrl;
}

View File

@@ -40,7 +40,6 @@ import {
NOTIFICATION_TIMEOUT_TYPE
} from '../notifications/constants';
import { showStartRecordingNotification } from '../recording/actions';
import { showSalesforceNotification } from '../salesforce/actions';
import { setToolboxEnabled } from '../toolbox/actions.any';
import { DISMISS_CALENDAR_NOTIFICATION } from './actionTypes';
@@ -164,7 +163,6 @@ function _conferenceJoined({ dispatch, getState }: IStore) {
}), 10 * 1000);
}
dispatch(showSalesforceNotification());
dispatch(showStartRecordingNotification());
_checkIframe(getState(), dispatch);

View File

@@ -31,9 +31,6 @@ import StartLiveStreamDialog from '../../../../../recording/components/LiveStrea
import StartRecordingDialog
// @ts-ignore
from '../../../../../recording/components/Recording/native/StartRecordingDialog';
import SalesforceLinkDialog
// @ts-ignore
from '../../../../../salesforce/components/native/SalesforceLinkDialog';
import SecurityDialog
// @ts-ignore
from '../../../../../security/components/security-dialog/native/SecurityDialog';
@@ -58,7 +55,6 @@ import {
navigationContainerTheme,
participantsScreenOptions,
recordingScreenOptions,
salesforceScreenOptions,
securityScreenOptions,
settingsNavigationContainerScreenOptions,
sharedDocumentScreenOptions,
@@ -154,13 +150,6 @@ const ConferenceNavigationContainer = () => {
...speakerStatsScreenOptions,
title: t('speakerStats.speakerStats')
}} />
<ConferenceStack.Screen
component = { SalesforceLinkDialog }
name = { screen.conference.salesforce }
options = {{
...salesforceScreenOptions,
title: t('notify.linkToSalesforce')
}} />
<ConferenceStack.Screen
component = { GifsMenu }
name = { screen.conference.gifsMenu }

View File

@@ -18,7 +18,6 @@ export const screen = {
participants: 'Participants',
root: 'Conference root',
recording: 'Recording',
salesforce: 'Link to Salesforce',
security: 'Security Options',
sharedDocument: 'Shared document',
speakerStats: 'Speaker Stats',

View File

@@ -165,11 +165,6 @@ export const lobbyChatScreenOptions = {
headerLeft: () => screenHeaderCloseButton(goBackToLobbyScreen)
};
/**
* Screen options for salesforce link modal.
*/
export const salesforceScreenOptions = presentationScreenOptions;
/**
* Screen options for GIPHY integration modal.
*/

View File

@@ -98,13 +98,6 @@ export const LOCAL_RECORDING_NOTIFICATION_ID = 'LOCAL_RECORDING_NOTIFICATION_ID'
*/
export const RAISE_HAND_NOTIFICATION_ID = 'RAISE_HAND_NOTIFICATION';
/**
* The identifier of the salesforce link notification.
*
* @type {string}
*/
export const SALESFORCE_LINK_NOTIFICATION_ID = 'SALESFORCE_LINK_NOTIFICATION';
/**
* The identifier of the visitors promotion notification.
*

View File

@@ -1,36 +0,0 @@
import { IStore } from '../app/types';
import { openDialog } from '../base/dialog/actions';
import { hideNotification, showNotification } from '../notifications/actions';
import {
NOTIFICATION_TIMEOUT_TYPE,
NOTIFICATION_TYPE,
SALESFORCE_LINK_NOTIFICATION_ID
} from '../notifications/constants';
import { SalesforceLinkDialog } from './components';
import { isSalesforceEnabled } from './functions';
/**
* Displays the notification for linking the meeting to Salesforce.
*
* @returns {void}
*/
export function showSalesforceNotification() {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (!isSalesforceEnabled(getState())) {
return;
}
dispatch(showNotification({
descriptionKey: 'notify.linkToSalesforceDescription',
titleKey: 'notify.linkToSalesforce',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
customActionNameKey: [ 'notify.linkToSalesforceKey' ],
customActionHandler: [ () => {
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(openDialog(SalesforceLinkDialog));
} ],
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
};
}

View File

@@ -1,2 +0,0 @@
// @ts-ignore
export { default as SalesforceLinkDialog } from './native/SalesforceLinkDialog';

View File

@@ -1 +0,0 @@
export { default as SalesforceLinkDialog } from './web/SalesforceLinkDialog';

View File

@@ -1,83 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { GestureResponderEvent, Text, TextStyle, TouchableHighlight, View, ViewStyle } from 'react-native';
import Icon from '../../../base/icons/components/Icon';
import { RECORD_TYPE } from '../../constants';
import styles from './styles';
/**
* The type of the React {@code Component} props of {@link RecordItem}.
*/
interface IProps {
/**
* The id of the record.
*/
id?: string;
/**
* The name of the record.
*/
name?: string;
/**
* The handler for the click event.
*/
onClick?: (e?: GestureResponderEvent | React.MouseEvent) => void;
/**
* The type of the record.
*/
type?: string;
}
/**
* Component to render Record data.
*
* @param {IProps} props - The props of the component.
* @returns {React$Element<any>}
*/
export const RecordItem = ({
id,
name,
type,
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
onClick = () => {}
}: IProps) => {
const { t } = useTranslation();
const IconRecord = RECORD_TYPE[type ?? ''].icon;
return (
<TouchableHighlight onPress = { onClick }>
<View
key = { `record-${id}` }
style = { styles.recordItem as ViewStyle }
// @ts-ignore
title = { name }>
<View style = { styles.recordTypeIcon as ViewStyle }>
{IconRecord && (
<Icon
src = { IconRecord }
style = { styles.recordIcon } />
)}
</View>
<View style = { styles.recordDetails as ViewStyle }>
<Text
key = { name }
numberOfLines = { 1 }
style = { styles.recordName as TextStyle }>
{name}
</Text>
<Text
key = { type }
style = { styles.recordType }>
{t(RECORD_TYPE[type ?? ''].label)}
</Text>
</View>
</View>
</TouchableHighlight>
);
};

View File

@@ -1,186 +0,0 @@
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Platform, SafeAreaView, ScrollView, Text, View, ViewStyle } from 'react-native';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../app/types';
import { IconSearch } from '../../../base/icons/svg';
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
import Button from '../../../base/ui/components/native/Button';
import Input from '../../../base/ui/components/native/Input';
import { BUTTON_TYPES } from '../../../base/ui/constants.native';
import { navigate } from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { CONTENT_HEIGHT_OFFSET, LIST_HEIGHT_OFFSET, NOTES_LINES, NOTES_MAX_LENGTH } from '../../constants';
import { useSalesforceLinkDialog } from '../../useSalesforceLinkDialog';
import { RecordItem } from './RecordItem';
import styles from './styles';
/**
* Component that renders the Salesforce link dialog.
*
* @returns {React$Element<any>}
*/
const SalesforceLinkDialog = () => {
const { t } = useTranslation();
const { clientHeight } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
const {
hasDetailsErrors,
hasRecordsErrors,
isLoading,
linkMeeting,
notes,
records,
searchTerm,
selectedRecord,
selectedRecordOwner,
setNotes,
setSearchTerm,
setSelectedRecord,
showNoResults,
showSearchResults
} = useSalesforceLinkDialog();
const handlePress = useCallback(() => {
navigate(screen.conference.main);
selectedRecord && linkMeeting();
}, [ navigate, linkMeeting ]);
const renderSpinner = () => (
<View style = { [ styles.recordsSpinner, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<LoadingIndicator />
</View>
);
const renderDetailsErrors = () => (
<Text style = { styles.detailsError }>
{t('dialog.searchResultsDetailsError')}
</Text>
);
const renderSelection = () => (
<SafeAreaView>
<ScrollView
bounces = { false }
style = { [ styles.selectedRecord, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<View style = { styles.recordInfo as ViewStyle }>
<RecordItem { ...selectedRecord } />
{ selectedRecordOwner && <RecordItem { ...selectedRecordOwner } /> }
{ hasDetailsErrors && renderDetailsErrors() }
</View>
<Text style = { styles.addNote }>
{t('dialog.addOptionalNote')}
</Text>
<Input
customStyles = {{ container: styles.notes }}
maxLength = { NOTES_MAX_LENGTH }
minHeight = { Platform.OS === 'ios' && NOTES_LINES ? 20 * NOTES_LINES : undefined }
multiline = { true }
numberOfLines = { Platform.OS === 'ios' ? undefined : NOTES_LINES }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { value => setNotes(value) }
placeholder = { t('dialog.addMeetingNote') }
value = { notes } />
</ScrollView>
</SafeAreaView>
);
const renderRecordsSearch = () => (
<View style = { styles.recordsSearchContainer as ViewStyle }>
<Input
icon = { IconSearch }
maxLength = { NOTES_MAX_LENGTH }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { value => setSearchTerm(value) }
placeholder = { t('dialog.searchInSalesforce') }
value = { searchTerm ?? '' } />
{(!isLoading && !hasRecordsErrors) && (
<Text style = { styles.resultLabel }>
{showSearchResults
? t('dialog.searchResults', { count: records.length })
: t('dialog.recentlyUsedObjects')
}
</Text>
)}
</View>
);
const renderNoRecords = () => showNoResults && (
<View style = { [ styles.noRecords, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<Text style = { styles.noRecordsText }>
{t('dialog.searchResultsNotFound')}
</Text>
<Text style = { styles.noRecordsText }>
{t('dialog.searchResultsTryAgain')}
</Text>
</View>
);
const renderRecordsError = () => (
<View style = { [ styles.recordsError, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] as ViewStyle[] }>
<Text style = { styles.recordsErrorText }>
{t('dialog.searchResultsError')}
</Text>
</View>
);
const renderContent = () => {
if (isLoading) {
return renderSpinner();
}
if (hasRecordsErrors) {
return renderRecordsError();
}
if (showNoResults) {
return renderNoRecords();
}
if (selectedRecord) {
return renderSelection();
}
return (
<SafeAreaView>
<ScrollView
bounces = { false }
style = { [ styles.recordList, { height: clientHeight - LIST_HEIGHT_OFFSET } ] as ViewStyle[] }>
{records.map((item: any) => (
<RecordItem
key = { `record-${item.id}` }
/* eslint-disable-next-line react/jsx-no-bind */
onClick = { () => setSelectedRecord(item) }
{ ...item } />
))}
</ScrollView>
</SafeAreaView>
);
};
return (
<JitsiScreen style = { styles.salesforceDialogContainer }>
<View>
{!selectedRecord && renderRecordsSearch()}
{renderContent()}
</View>
{
selectedRecord
&& <View>
<Button
labelKey = 'dialog.Cancel'
/* eslint-disable-next-line react/jsx-no-bind */
onClick = { () => setSelectedRecord(null) }
style = { styles.cancelButton }
type = { BUTTON_TYPES.SECONDARY } />
<Button
labelKey = 'dialog.linkMeeting'
onClick = { handlePress }
style = { styles.linkButton }
type = { BUTTON_TYPES.PRIMARY } />
</View>
}
</JitsiScreen>
);
};
export default SalesforceLinkDialog;

View File

@@ -1,148 +0,0 @@
import BaseTheme from '../../../base/ui/components/BaseTheme.native';
export default {
salesforceDialogContainer: {
position: 'relative',
flexDirection: 'column',
flex: 1,
display: 'flex',
backgroundColor: BaseTheme.palette.ui01
},
recordsSearchContainer: {
alignSelf: 'stretch',
backgroundColor: BaseTheme.palette.ui01,
paddingHorizontal: BaseTheme.spacing[3],
paddingTop: BaseTheme.spacing[2],
position: 'relative'
},
searchIcon: {
color: BaseTheme.palette.text03,
fontSize: 30,
left: 22,
position: 'absolute',
top: 22,
zIndex: 2
},
resultLabel: {
backgroundColor: BaseTheme.palette.ui01,
color: BaseTheme.palette.text03,
fontSize: 15,
margin: 0,
paddingBottom: 8,
paddingTop: 16
},
recordsSpinner: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center',
width: '100%'
},
noRecords: {
alignItems: 'center',
backgroundColor: BaseTheme.palette.ui01,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: BaseTheme.spacing[3]
},
noRecordsText: {
color: BaseTheme.palette.text03
},
recordsError: {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
paddingBottom: 30
},
recordsErrorText: {
color: BaseTheme.palette.text03
},
recordList: {
alignSelf: 'stretch',
display: 'flex',
listStyle: 'none',
paddingVertical: BaseTheme.spacing[3],
position: 'relative'
},
selectedRecord: {
alignSelf: 'stretch',
display: 'flex',
paddingTop: BaseTheme.spacing[3],
position: 'relative'
},
recordInfo: {
backgroundColor: BaseTheme.palette.ui03,
borderRadius: BaseTheme.shape.borderRadius,
display: 'flex',
margin: BaseTheme.spacing[3],
position: 'relative'
},
detailsError: {
color: BaseTheme.palette.text03,
padding: BaseTheme.spacing[3]
},
addNote: {
color: BaseTheme.palette.text01,
margin: BaseTheme.spacing[3]
},
notes: {
alignItems: 'flex-start',
backgroundColor: BaseTheme.palette.field01,
borderColor: BaseTheme.palette.ui05,
borderRadius: BaseTheme.shape.borderRadius,
borderWidth: 1,
color: BaseTheme.palette.text01,
lineHeight: 18,
marginHorizontal: BaseTheme.spacing[3],
marginVertical: BaseTheme.spacing[2],
overflow: 'hidden',
padding: BaseTheme.spacing[2],
textAlignVertical: 'top'
},
cancelButton: {
margin: BaseTheme.spacing[2]
},
linkButton: {
marginBottom: BaseTheme.spacing[2],
marginHorizontal: BaseTheme.spacing[2]
},
recordItem: {
alignItems: 'center',
display: 'flex',
flex: 1,
flexDirection: 'row',
paddingHorizontal: BaseTheme.spacing[3]
},
recordTypeIcon: {
alignItems: 'center',
borderRadius: BaseTheme.shape.borderRadius,
display: 'flex',
height: 40,
justifyContent: 'center',
marginRight: BaseTheme.spacing[3],
width: 40
},
recordIcon: {
alignItems: 'center',
display: 'flex',
justifyContent: 'center'
},
recordDetails: {
display: 'flex',
flex: 1,
flexDirection: 'column',
justifyContent: 'space-around',
overflow: 'hidden',
paddingVertical: BaseTheme.spacing[3]
},
recordName: {
color: BaseTheme.palette.text01,
fontSize: 15,
overflow: 'hidden'
},
recordType: {
color: BaseTheme.palette.text01,
fontSize: 13
}
};

View File

@@ -1,107 +0,0 @@
import React from 'react';
import { useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { RECORD_TYPE } from '../../constants';
/**
* The type of the React {@code Component} props of {@link RecordItem}.
*/
interface IProps {
/**
* The id of the record.
*/
id?: string;
/**
* The name of the record.
*/
name?: string;
/**
* The handler for the click event.
*/
onClick?: (e?: React.MouseEvent) => void;
/**
* The type of the record.
*/
type?: string;
}
const useStyles = makeStyles()(theme => {
return {
recordItem: {
display: 'flex',
alignItems: 'center'
},
recordTypeIcon: {
borderRadius: theme.shape.borderRadius,
height: '40px',
marginRight: '16px',
width: '40px'
},
recordDetails: {
display: 'flex',
flex: 1,
flexDirection: 'column',
justifyContent: 'space-around',
overflow: 'hidden',
padding: '12px 0',
textOverflow: 'ellipsis'
},
recordName: {
fontSize: '15px',
fontWeight: 400,
lineHeight: '20px',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
recordType: {
fontSize: '13px',
fontWeight: 400,
lineHeight: '18px'
}
};
});
/**
* Component to render Record data.
*
* @param {IProps} props - The props of the component.
* @returns {React$Element<any>}
*/
export const RecordItem = ({
id,
name,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick = () => {},
type
}: IProps) => {
const { t } = useTranslation();
const Icon = RECORD_TYPE[type as keyof typeof RECORD_TYPE].icon;
const { classes } = useStyles();
return (
<li
className = { classes.recordItem }
key = { `record-${id}` }
onClick = { onClick }
title = { name }>
<div className = { classes.recordTypeIcon }>{Icon && <Icon />}</div>
<div className = { classes.recordDetails }>
<div
className = { classes.recordName }
key = { name }>
{name}
</div>
<div
className = { classes.recordType }
key = { type }>
{t(RECORD_TYPE[type ?? ''].label)}
</div>
</div>
</li>
);
};

View File

@@ -1,285 +0,0 @@
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 { withPixelLineHeight } from '../../../base/styles/functions.web';
import Dialog from '../../../base/ui/components/web/Dialog';
import Spinner from '../../../base/ui/components/web/Spinner';
import { NOTES_MAX_LENGTH } from '../../constants';
import { useSalesforceLinkDialog } from '../../useSalesforceLinkDialog';
import { RecordItem } from './RecordItem';
const useStyles = makeStyles()(theme => {
return {
container: {
height: '450px',
overflowY: 'auto',
position: 'relative'
},
recordsSearchContainer: {
position: 'relative',
padding: '1px'
},
searchIcon: {
display: 'block',
position: 'absolute',
color: theme.palette.text03,
left: 16,
top: 10,
width: 20,
height: 20
},
resultLabel: {
fontSize: '15px',
margin: '16px 0 8px'
},
recordsSearch: {
backgroundColor: theme.palette.field01,
border: '1px solid',
borderRadius: theme.shape.borderRadius,
borderColor: theme.palette.ui05,
color: theme.palette.text01,
padding: '10px 16px 10px 44px',
width: '100%',
height: 40,
'&::placeholder': {
color: theme.palette.text03,
...withPixelLineHeight(theme.typography.bodyShortRegular)
}
},
spinner: {
alignItems: 'center',
display: 'flex',
height: 'calc(100% - 70px)',
justifyContent: 'center',
width: '100%',
'@media (max-width: 448px)': {
height: 'auto',
marginTop: '24px'
}
},
noRecords: {
height: 'calc(100% - 150px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
'@media (max-width: 448px)': {
height: 'auto',
marginTop: '24px'
}
},
recordsError: {
height: 'calc(100% - 42px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'column',
'@media (max-width: 448px)': {
height: 'auto',
marginTop: '24px'
}
},
recordList: {
listStyle: 'none',
margin: '10px 0',
padding: 0
},
recordInfo: {
backgroundColor: theme.palette.ui03,
padding: '0 16px',
borderRadius: theme.shape.borderRadius,
marginBottom: '28px'
},
detailsError: {
padding: '10px 0'
},
addNote: {
padding: '10px 0'
},
notes: {
lineHeight: '18px',
minHeight: '130px',
resize: 'vertical',
width: '100%',
boxSizing: 'border-box',
overflow: 'hidden',
border: '1px solid',
borderColor: theme.palette.ui05,
backgroundColor: theme.palette.field01,
color: theme.palette.text01,
borderRadius: theme.shape.borderRadius,
padding: '10px 16px'
}
};
});
/**
* Component that renders the Salesforce link dialog.
*
* @returns {React$Element<any>}
*/
function SalesforceLinkDialog() {
const { t } = useTranslation();
const { classes, theme } = useStyles();
const dispatch = useDispatch();
const {
hasDetailsErrors,
hasRecordsErrors,
isLoading,
linkMeeting,
notes,
records,
searchTerm,
selectedRecord,
selectedRecordOwner,
setNotes,
setSearchTerm,
setSelectedRecord,
showNoResults,
showSearchResults
} = useSalesforceLinkDialog();
const handleChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const value = getFieldValue(event);
setSearchTerm(value);
}, [ getFieldValue ]);
const handleSubmit = useCallback(() => {
dispatch(hideDialog());
selectedRecord && linkMeeting();
}, [ hideDialog, linkMeeting ]);
const renderSpinner = () => (
<div className = { classes.spinner }>
<Spinner />
</div>
);
const renderDetailsErrors = () => (
<div className = { classes.detailsError }>
{t('dialog.searchResultsDetailsError')}
</div>
);
const renderSelection = () => (
<div>
<div className = { classes.recordInfo }>
<RecordItem { ...selectedRecord } />
{selectedRecordOwner && <RecordItem { ...selectedRecordOwner } />}
{hasDetailsErrors && renderDetailsErrors()}
</div>
<div className = { classes.addNote }>{t('dialog.addOptionalNote')}</div>
<textarea
autoFocus = { true }
className = { classes.notes }
maxLength = { NOTES_MAX_LENGTH }
/* eslint-disable-next-line react/jsx-no-bind */
onChange = { e => setNotes(e.target.value) }
placeholder = { t('dialog.addMeetingNote') }
rows = { 4 }
value = { notes } />
</div>
);
const renderRecordsSearch = () => !selectedRecord && (
<div className = { classes.recordsSearchContainer }>
<Icon
className = { classes.searchIcon }
color = { theme.palette.icon03 }
src = { IconSearch } />
<input
autoComplete = 'off'
autoFocus = { false }
className = { classes.recordsSearch }
name = 'recordsSearch'
onChange = { handleChange }
placeholder = { t('dialog.searchInSalesforce') }
tabIndex = { 0 }
value = { searchTerm ?? '' } />
{(!isLoading && !hasRecordsErrors) && (
<div className = { classes.resultLabel }>
{showSearchResults
? t('dialog.searchResults', { count: records.length })
: t('dialog.recentlyUsedObjects')
}
</div>
)}
</div>
);
const renderNoRecords = () => showNoResults && (
<div className = { classes.noRecords }>
<div>{t('dialog.searchResultsNotFound')}</div>
<div>{t('dialog.searchResultsTryAgain')}</div>
</div>
);
const renderRecordsError = () => (
<div className = { classes.recordsError }>
{t('dialog.searchResultsError')}
</div>
);
const renderContent = () => {
if (isLoading) {
return renderSpinner();
}
if (hasRecordsErrors) {
return renderRecordsError();
}
if (showNoResults) {
return renderNoRecords();
}
if (selectedRecord) {
return renderSelection();
}
return (
<ul className = { classes.recordList }>
{records.map((item: any) => (
<RecordItem
key = { `record-${item.id}` }
/* eslint-disable-next-line react/jsx-no-bind */
onClick = { () => setSelectedRecord(item) }
{ ...item } />
))}
</ul>
);
};
return (
<Dialog
back = {{
hidden: !selectedRecord,
onClick: () => setSelectedRecord(null),
translationKey: 'dialog.Back'
}}
cancel = {{ hidden: true }}
disableEnter = { true }
ok = {{
translationKey: 'dialog.linkMeeting',
hidden: !selectedRecord
}}
onSubmit = { handleSubmit }
titleKey = 'dialog.linkMeetingTitle'>
<div className = { classes.container } >
{renderRecordsSearch()}
{renderContent()}
</div>
</Dialog>
);
}
export default SalesforceLinkDialog;

View File

@@ -1,41 +0,0 @@
import {
IconRecordAccount,
IconRecordContact,
IconRecordLead,
IconRecordOpportunity
} from '../base/icons/svg';
export const NOTES_MAX_LENGTH = 255;
export const NOTES_LINES = 4;
export const CONTENT_HEIGHT_OFFSET = 200;
export const LIST_HEIGHT_OFFSET = 250;
export const RECORD_TYPE: {
[key: string]: {
icon?: Function;
label: string;
};
} = {
ACCOUNT: {
label: 'record.type.account',
icon: IconRecordAccount
},
CONTACT: {
label: 'record.type.contact',
icon: IconRecordContact
},
LEAD: {
label: 'record.type.lead',
icon: IconRecordLead
},
OPPORTUNITY: {
label: 'record.type.opportunity',
icon: IconRecordOpportunity
},
OWNER: {
label: 'record.type.owner'
}
};

View File

@@ -1,114 +0,0 @@
import { IReduxState } from '../app/types';
import { doGetJSON } from '../base/util/httpUtils';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
/**
* Determines whether Salesforce is enabled for the current conference.
*
* @param {IReduxState} state - The redux store, the redux
* {@code getState} function, or the redux state itself.
* @returns {boolean}
*/
export const isSalesforceEnabled = (state: IReduxState) => {
const { salesforceUrl } = state['features/base/config'];
const isBreakoutRoom = isInBreakoutRoom(state);
return Boolean(salesforceUrl) && !isBreakoutRoom;
};
/**
* Fetches the Salesforce records that were most recently interacted with.
*
* @param {string} url - The endpoint for the session records.
* @param {string} jwt - The JWT needed for authentication.
* @returns {Promise<any>}
*/
export async function getRecentSessionRecords(
url: string,
jwt: string
) {
return doGetJSON(`${url}/records/recents`, true, {
headers: {
'Authorization': `Bearer ${jwt}`
}
});
}
/**
* Fetches the Salesforce records that match the search criteria.
*
* @param {string} url - The endpoint for the session records.
* @param {string} jwt - The JWT needed for authentication.
* @param {string} text - The search term for the session record to find.
* @returns {Promise<any>}
*/
export async function searchSessionRecords(
url: string,
jwt: string,
text: string
) {
return doGetJSON(`${url}/records?text=${text}`, true, {
headers: {
'Authorization': `Bearer ${jwt}`
}
});
}
/**
* Fetches the Salesforce record details from the server.
*
* @param {string} url - The endpoint for the record details.
* @param {string} jwt - The JWT needed for authentication.
* @param {Object} item - The item for which details are being retrieved.
* @returns {Promise<any>}
*/
export async function getSessionRecordDetails(
url: string,
jwt: string,
item: {
id: string;
type: string;
} | null
) {
const fullUrl = `${url}/records/${item?.id}?type=${item?.type}`;
return doGetJSON(fullUrl, true, {
headers: {
'Authorization': `Bearer ${jwt}`
}
});
}
/**
* Executes the meeting linking.
*
* @param {string} url - The endpoint for meeting linking.
* @param {string} jwt - The JWT needed for authentication.
* @param {string} sessionId - The ID of the meeting session.
* @param {Object} body - The body of the request.
* @returns {Object}
*/
export async function executeLinkMeetingRequest(
url: string,
jwt: string,
sessionId: String,
body: {
id?: string;
notes: string;
type?: string;
}
) {
const fullUrl = `${url}/sessions/${sessionId}/records/${body.id}`;
const res = await fetch(fullUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwt}`
},
body: JSON.stringify(body)
});
const json = await res.json();
return res.ok ? json : Promise.reject(json);
}

View File

@@ -1,156 +0,0 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { GestureResponderEvent } from 'react-native';
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 {
executeLinkMeetingRequest,
getRecentSessionRecords,
getSessionRecordDetails,
searchSessionRecords
} from './functions';
interface ISelectedRecord {
id: string;
name: string;
onClick: (e?: React.MouseEvent | GestureResponderEvent) => void;
type: string;
}
export const useSalesforceLinkDialog = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
const [ selectedRecord, setSelectedRecord ] = useState<ISelectedRecord | null>(null);
const [ selectedRecordOwner, setSelectedRecordOwner ] = useState<{
id: string; name: string; type: string; } | null>(null);
const [ records, setRecords ] = useState([]);
const [ isLoading, setLoading ] = useState(false);
const [ searchTerm, setSearchTerm ] = useState<string | null>(null);
const [ notes, setNotes ] = useState('');
const [ hasRecordsErrors, setRecordsErrors ] = useState(false);
const [ hasDetailsErrors, setDetailsErrors ] = useState(false);
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']);
const showSearchResults = searchTerm && searchTerm.length > 1;
const showNoResults = showSearchResults && records.length === 0;
useEffect(() => {
const fetchRecords = async () => {
setRecordsErrors(false);
setLoading(true);
try {
const text = showSearchResults ? searchTerm : null;
const result = text
? await searchSessionRecords(salesforceUrl, jwt, text)
: await getRecentSessionRecords(salesforceUrl, jwt);
setRecords(result);
} catch (error) {
setRecordsErrors(true);
}
setLoading(false);
};
fetchRecords();
}, [
getRecentSessionRecords,
jwt,
salesforceUrl,
searchSessionRecords,
searchTerm
]);
useEffect(() => {
const fetchRecordDetails = async () => {
setDetailsErrors(false);
setSelectedRecordOwner(null);
try {
const result = await getSessionRecordDetails(salesforceUrl, jwt, selectedRecord);
setSelectedRecordOwner({
id: result.id,
name: result.ownerName,
type: 'OWNER'
});
} catch (error) {
setDetailsErrors(true);
}
};
selectedRecord && fetchRecordDetails();
}, [
jwt,
getSessionRecordDetails,
salesforceUrl,
selectedRecord
]);
const linkMeeting = useCallback(async () => {
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceProgress',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.NORMAL
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
try {
await executeLinkMeetingRequest(salesforceUrl, jwt, sessionId, {
id: selectedRecord?.id,
type: selectedRecord?.type,
notes
});
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
} catch (error: any) {
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceError',
descriptionKey: error?.messageKey && t(error.messageKey),
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
}
}, [
executeLinkMeetingRequest,
hideNotification,
jwt,
notes,
salesforceUrl,
selectedRecord,
showNotification
]);
return {
hasDetailsErrors,
hasRecordsErrors,
isLoading,
linkMeeting,
notes,
records,
searchTerm,
selectedRecord,
selectedRecordOwner,
setNotes,
setSearchTerm,
setSelectedRecord,
showNoResults,
showSearchResults
};
};

View File

@@ -1,48 +0,0 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import { navigate }
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
import { screen } from '../../../mobile/navigation/routes';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
sendAnalytics(createToolbarEvent('link.to.salesforce'));
return navigate(screen.conference.salesforce);
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @private
* @returns {Props}
*/
function mapStateToProps(state: IReduxState) {
return {
visible: isSalesforceEnabled(state)
};
}
export default translate(connect(mapStateToProps)(LinkToSalesforceButton));

View File

@@ -31,7 +31,6 @@ import { useNativeToolboxButtons } from '../../hooks.native';
import { IToolboxNativeButton } from '../../types';
import AudioOnlyButton from './AudioOnlyButton';
import LinkToSalesforceButton from './LinkToSalesforceButton';
import OpenCarmodeButton from './OpenCarmodeButton';
import RaiseHandButton from './RaiseHandButton';
@@ -162,7 +161,6 @@ class OverflowMenu extends PureComponent<IProps, IState> {
<SecurityDialogButton { ...buttonProps } />
<RecordButton { ...buttonProps } />
<LiveStreamButton { ...buttonProps } />
<LinkToSalesforceButton { ...buttonProps } />
<WhiteboardButton { ...buttonProps } />
{/* @ts-ignore */}
<Divider style = { styles.divider as ViewStyle } />

View File

@@ -1,48 +0,0 @@
import { connect } from 'react-redux';
import { createToolbarEvent } from '../../../analytics/AnalyticsEvents';
import { sendAnalytics } from '../../../analytics/functions';
import { IReduxState } from '../../../app/types';
import { openDialog } from '../../../base/dialog/actions';
import { translate } from '../../../base/i18n/functions';
import { IconCloudUpload } from '../../../base/icons/svg';
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
import SalesforceLinkDialog from '../../../salesforce/components/web/SalesforceLinkDialog';
import { isSalesforceEnabled } from '../../../salesforce/functions';
/**
* Implementation of a button for opening the Salesforce link dialog.
*/
class LinkToSalesforce extends AbstractButton<AbstractButtonProps> {
override accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce';
override icon = IconCloudUpload;
override label = 'toolbar.linkToSalesforce';
override tooltip = 'toolbar.linkToSalesforce';
/**
* Handles clicking / pressing the button, and opens the Salesforce link dialog.
*
* @protected
* @returns {void}
*/
override _handleClick() {
const { dispatch } = this.props;
sendAnalytics(createToolbarEvent('link.to.salesforce'));
dispatch(openDialog(SalesforceLinkDialog));
}
}
/**
* Function that maps parts of Redux state tree into component props.
*
* @param {Object} state - Redux state.
* @returns {Object}
*/
const mapStateToProps = (state: IReduxState) => {
return {
visible: isSalesforceEnabled(state)
};
};
export default translate(connect(mapStateToProps)(LinkToSalesforce));

View File

@@ -107,7 +107,6 @@ export const MAIN_TOOLBAR_BUTTONS_PRIORITY = [
'closedcaptions',
'recording',
'livestreaming',
'linktosalesforce',
'sharedvideo',
'shareaudio',
'noisesuppression',
@@ -157,7 +156,6 @@ export const TOOLBAR_BUTTONS: ToolbarButton[] = [
'help',
'highlight',
'invite',
'linktosalesforce',
'livestreaming',
'microphone',
'mute-everyone',

View File

@@ -39,7 +39,6 @@ import { REACTIONS } from '../reactions/constants';
import { shouldDisplayReactionsButtons } from '../reactions/functions.any';
import { useReactionsButton } from '../reactions/hooks.web';
import { useLiveStreamingButton, useRecordingButton } from '../recording/hooks.web';
import { isSalesforceEnabled } from '../salesforce/functions';
import { startScreenShareFlow } from '../screen-share/actions.web';
import ShareAudioButton from '../screen-share/components/web/ShareAudioButton';
import { isScreenAudioSupported, isScreenVideoShared } from '../screen-share/functions';
@@ -64,7 +63,6 @@ import HelpButton from './components/HelpButton';
import AudioSettingsButton from './components/web/AudioSettingsButton';
import CustomOptionButton from './components/web/CustomOptionButton';
import FullscreenButton from './components/web/FullscreenButton';
import LinkToSalesforceButton from './components/web/LinkToSalesforceButton';
import ProfileButton from './components/web/ProfileButton';
import ShareDesktopButton from './components/web/ShareDesktopButton';
import ToggleCameraButton from './components/web/ToggleCameraButton';
@@ -136,12 +134,6 @@ const fullscreen = {
group: 2
};
const linkToSalesforce = {
key: 'linktosalesforce',
Content: LinkToSalesforceButton,
group: 2
};
const shareAudio = {
key: 'shareaudio',
Content: ShareAudioButton,
@@ -207,20 +199,6 @@ function getFullscreenButton() {
}
}
/**
* A hook that returns the "link to salesforce" button if it is enabled and undefined otherwise.
*
* @returns {Object | undefined}
*/
function useLinkToSalesforceButton() {
const _isSalesforceEnabled = useSelector(isSalesforceEnabled);
if (_isSalesforceEnabled) {
return linkToSalesforce;
}
}
/**
* A hook that returns the share audio button if it is enabled and undefined otherwise.
*
@@ -280,7 +258,6 @@ export function useToolboxButtons(
const cc = useClosedCaptionButton();
const recording = useRecordingButton();
const liveStreaming = useLiveStreamingButton();
const linktosalesforce = useLinkToSalesforceButton();
const shareaudio = getShareAudioButton();
const shareVideo = useSharedVideoButton();
const whiteboard = useWhiteboardButton();
@@ -311,7 +288,6 @@ export function useToolboxButtons(
closedcaptions: cc,
recording,
livestreaming: liveStreaming,
linktosalesforce,
sharedvideo: shareVideo,
shareaudio,
noisesuppression: noiseSuppression,

View File

@@ -30,7 +30,6 @@ export type ToolbarButton = 'camera' |
'help' |
'highlight' |
'invite' |
'linktosalesforce' |
'livestreaming' |
'microphone' |
'mute-everyone' |