mirror of https://github.com/jitsi/jitsi-meet
parent
9d8ae922a9
commit
98256a8d5b
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 936 B |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 949 B |
@ -0,0 +1,39 @@ |
||||
// @flow
|
||||
|
||||
import { openDialog } from '../base/dialog'; |
||||
import { |
||||
hideNotification, |
||||
NOTIFICATION_TIMEOUT_TYPE, |
||||
NOTIFICATION_TYPE, |
||||
SALESFORCE_LINK_NOTIFICATION_ID, |
||||
showNotification |
||||
} from '../notifications'; |
||||
|
||||
import { SalesforceLinkDialog } from './components'; |
||||
|
||||
/** |
||||
* Displays the notification for linking the meeting to Salesforce. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
export function showSalesforceNotification() { |
||||
return (dispatch: Object, getState: Function) => { |
||||
const { salesforceUrl } = getState()['features/base/config']; |
||||
|
||||
if (!salesforceUrl) { |
||||
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.STICKY)); |
||||
}; |
||||
} |
@ -0,0 +1 @@ |
||||
export * from './native'; |
@ -0,0 +1 @@ |
||||
export * from './web'; |
@ -0,0 +1 @@ |
||||
export * from './_'; |
@ -0,0 +1,83 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { View, Text, TouchableHighlight } from 'react-native'; |
||||
|
||||
import { Icon } from '../../../base/icons'; |
||||
import { RECORD_TYPE } from '../../constants'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link RecordItem}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The id of the record. |
||||
*/ |
||||
id: String, |
||||
|
||||
/** |
||||
* The name of the record. |
||||
*/ |
||||
name: String, |
||||
|
||||
/** |
||||
* The handler for the click event. |
||||
*/ |
||||
onClick: Function, |
||||
|
||||
/** |
||||
* The type of the record. |
||||
*/ |
||||
type: String |
||||
} |
||||
|
||||
/** |
||||
* Component to render Record data. |
||||
* |
||||
* @param {Props} props - The props of the component. |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
export const RecordItem = ({ |
||||
id, |
||||
name, |
||||
type, |
||||
/* eslint-disable-next-line no-empty-function */ |
||||
onClick = () => {} |
||||
}: Props) => { |
||||
const { t } = useTranslation(); |
||||
const IconRecord = RECORD_TYPE[type].icon; |
||||
|
||||
return ( |
||||
<TouchableHighlight onPress = { onClick }> |
||||
<View |
||||
key = { `record-${id}` } |
||||
style = { styles.recordItem } |
||||
title = { name }> |
||||
<View style = { styles.recordTypeIcon }> |
||||
{IconRecord && ( |
||||
<Icon |
||||
src = { IconRecord } |
||||
style = { styles.recordIcon } /> |
||||
)} |
||||
</View> |
||||
<View style = { styles.recordDetails }> |
||||
<Text |
||||
key = { name } |
||||
numberOfLines = { 1 } |
||||
style = { styles.recordName }> |
||||
{name} |
||||
</Text> |
||||
<Text |
||||
key = { type } |
||||
style = { styles.recordType }> |
||||
{t(RECORD_TYPE[type].label)} |
||||
</Text> |
||||
</View> |
||||
</View> |
||||
</TouchableHighlight> |
||||
); |
||||
}; |
@ -0,0 +1,192 @@ |
||||
// @flow
|
||||
|
||||
import React, { useCallback } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { View, SafeAreaView, ScrollView, Text, TextInput, Platform } from 'react-native'; |
||||
import { Button, withTheme } from 'react-native-paper'; |
||||
import { useSelector } from 'react-redux'; |
||||
|
||||
import { Icon, IconSearch } from '../../../base/icons'; |
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen'; |
||||
import { LoadingIndicator } from '../../../base/react'; |
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.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 => 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); |
||||
linkMeeting(); |
||||
}, [ navigate, linkMeeting ]); |
||||
|
||||
const renderSpinner = () => ( |
||||
<View style = { [ styles.recordsSpinner, { height: clientHeight - CONTENT_HEIGHT_OFFSET } ] }> |
||||
<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 } ] }> |
||||
<View style = { styles.recordInfo }> |
||||
<RecordItem { ...selectedRecord } /> |
||||
{selectedRecordOwner && <RecordItem { ...selectedRecordOwner } />} |
||||
{hasDetailsErrors && renderDetailsErrors()} |
||||
</View> |
||||
<Text style = { styles.addNote }> |
||||
{t('dialog.addOptionalNote')} |
||||
</Text> |
||||
<TextInput |
||||
maxLength = { NOTES_MAX_LENGTH } |
||||
minHeight = { Platform.OS === 'ios' && NOTES_LINES ? 20 * NOTES_LINES : null } |
||||
multiline = { true } |
||||
numberOfLines = { Platform.OS === 'ios' ? null : NOTES_LINES } |
||||
/* eslint-disable-next-line react/jsx-no-bind */ |
||||
onChangeText = { value => setNotes(value) } |
||||
placeholder = { t('dialog.addMeetingNote') } |
||||
placeholderTextColor = { BaseTheme.palette.text03 } |
||||
style = { styles.notes } |
||||
value = { notes } /> |
||||
</ScrollView> |
||||
</SafeAreaView> |
||||
); |
||||
|
||||
const renderRecordsSearch = () => ( |
||||
<View style = { styles.recordsSearchContainer }> |
||||
<Icon |
||||
color = { BaseTheme.palette.icon03 } |
||||
src = { IconSearch } |
||||
style = { styles.searchIcon } /> |
||||
<TextInput |
||||
maxLength = { NOTES_MAX_LENGTH } |
||||
/* eslint-disable-next-line react/jsx-no-bind */ |
||||
onChangeText = { value => setSearchTerm(value) } |
||||
placeholder = { t('dialog.searchInSalesforce') } |
||||
placeholderTextColor = { BaseTheme.palette.text03 } |
||||
style = { styles.recordsSearch } |
||||
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 } ] }> |
||||
<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 } ] }> |
||||
<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 } ] }> |
||||
{records.map(item => ( |
||||
<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 style = { styles.footer }> |
||||
<Button |
||||
children = { t('dialog.Cancel') } |
||||
mode = 'contained' |
||||
/* eslint-disable-next-line react/jsx-no-bind */ |
||||
onPress = { () => setSelectedRecord(null) } |
||||
style = { styles.cancelButton } /> |
||||
<Button |
||||
children = { t('dialog.linkMeeting') } |
||||
mode = 'contained' |
||||
onPress = { handlePress } |
||||
style = { styles.linkButton } /> |
||||
</View> |
||||
} |
||||
</JitsiScreen> |
||||
); |
||||
}; |
||||
|
||||
export default withTheme(SalesforceLinkDialog); |
@ -0,0 +1 @@ |
||||
export { default as SalesforceLinkDialog } from './SalesforceLinkDialog'; |
@ -0,0 +1,161 @@ |
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native'; |
||||
|
||||
export default { |
||||
salesforceDialogContainer: { |
||||
position: 'relative', |
||||
flexDirection: 'column', |
||||
flex: 1, |
||||
display: 'flex', |
||||
backgroundColor: BaseTheme.palette.ui01 |
||||
}, |
||||
recordsSearchContainer: { |
||||
paddingHorizontal: BaseTheme.spacing[3], |
||||
paddingTop: BaseTheme.spacing[3], |
||||
backgroundColor: BaseTheme.palette.ui01, |
||||
alignSelf: 'stretch', |
||||
position: 'relative', |
||||
marginTop: BaseTheme.spacing[3] |
||||
}, |
||||
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 |
||||
}, |
||||
recordsSearch: { |
||||
backgroundColor: BaseTheme.palette.field01, |
||||
borderColor: BaseTheme.palette.border02, |
||||
borderRadius: BaseTheme.shape.borderRadius, |
||||
borderWidth: 1, |
||||
color: BaseTheme.palette.text01, |
||||
paddingLeft: 44, |
||||
paddingRight: 16, |
||||
paddingVertical: 10, |
||||
width: '100%' |
||||
}, |
||||
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.field02, |
||||
margin: BaseTheme.spacing[3] |
||||
}, |
||||
notes: { |
||||
alignItems: 'flex-start', |
||||
backgroundColor: BaseTheme.palette.field01, |
||||
borderColor: BaseTheme.palette.border02, |
||||
borderRadius: BaseTheme.shape.borderRadius, |
||||
borderWidth: 1, |
||||
color: BaseTheme.palette.field02, |
||||
lineHeight: 18, |
||||
marginHorizontal: BaseTheme.spacing[3], |
||||
overflow: 'hidden', |
||||
padding: BaseTheme.spacing[2], |
||||
textAlignVertical: 'top' |
||||
}, |
||||
cancelButton: { |
||||
backgroundColor: BaseTheme.palette.action02, |
||||
margin: BaseTheme.spacing[2] |
||||
}, |
||||
linkButton: { |
||||
backgroundColor: BaseTheme.palette.action01, |
||||
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 |
||||
} |
||||
}; |
@ -0,0 +1,108 @@ |
||||
// @flow
|
||||
import { makeStyles } from '@material-ui/core'; |
||||
import React from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
|
||||
import { RECORD_TYPE } from '../../constants'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link RecordItem}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The id of the record. |
||||
*/ |
||||
id: String, |
||||
|
||||
/** |
||||
* The name of the record. |
||||
*/ |
||||
name: String, |
||||
|
||||
/** |
||||
* The type of the record. |
||||
*/ |
||||
type: String, |
||||
|
||||
/** |
||||
* The handler for the click event. |
||||
*/ |
||||
onClick: Function |
||||
} |
||||
|
||||
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 {Props} props - The props of the component. |
||||
* @returns {React$Element<any>} |
||||
*/ |
||||
export const RecordItem = ({ |
||||
id, |
||||
name, |
||||
/* eslint-disable-next-line no-empty-function */ |
||||
onClick = () => {}, |
||||
type |
||||
}: Props) => { |
||||
const { t } = useTranslation(); |
||||
const classes = useStyles(); |
||||
const Icon = RECORD_TYPE[type].icon; |
||||
|
||||
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> |
||||
); |
||||
}; |
@ -0,0 +1,270 @@ |
||||
// @flow
|
||||
|
||||
import Spinner from '@atlaskit/spinner'; |
||||
import { makeStyles } from '@material-ui/core'; |
||||
import React, { useCallback } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useDispatch } from 'react-redux'; |
||||
|
||||
import { Dialog, hideDialog } from '../../../base/dialog'; |
||||
import { Icon, IconSearch } from '../../../base/icons'; |
||||
import { getFieldValue } from '../../../base/react'; |
||||
import BaseTheme from '../../../base/ui/components/BaseTheme'; |
||||
import { NOTES_MAX_LENGTH } from '../../constants'; |
||||
import { useSalesforceLinkDialog } from '../../useSalesforceLinkDialog'; |
||||
|
||||
import { RecordItem } from './RecordItem'; |
||||
|
||||
const useStyles = makeStyles(theme => { |
||||
return { |
||||
container: { |
||||
minHeight: '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.border02, |
||||
color: theme.palette.text01, |
||||
padding: '10px 16px 10px 44px', |
||||
width: '100%', |
||||
height: 40, |
||||
'&::placeholder': { |
||||
color: theme.palette.text03, |
||||
...theme.typography.bodyShortRegular, |
||||
lineHeight: `${theme.typography.bodyShortRegular.lineHeight}px` |
||||
} |
||||
}, |
||||
spinner: { |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
height: 'calc(100% - 100px)', |
||||
justifyContent: 'center', |
||||
width: '100%' |
||||
}, |
||||
noRecords: { |
||||
height: 'calc(100% - 150px)', |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
flexDirection: 'column' |
||||
}, |
||||
recordsError: { |
||||
height: 'calc(100% - 80px)', |
||||
display: 'flex', |
||||
alignItems: 'center', |
||||
justifyContent: 'center', |
||||
flexDirection: 'column' |
||||
}, |
||||
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: 'borderBox', |
||||
overflow: 'hidden', |
||||
border: '1px solid', |
||||
borderColor: theme.palette.border02, |
||||
backgroundColor: theme.palette.field01, |
||||
color: theme.palette.field02, |
||||
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 = useStyles(); |
||||
const dispatch = useDispatch(); |
||||
const { |
||||
hasDetailsErrors, |
||||
hasRecordsErrors, |
||||
isLoading, |
||||
linkMeeting, |
||||
notes, |
||||
records, |
||||
searchTerm, |
||||
selectedRecord, |
||||
selectedRecordOwner, |
||||
setNotes, |
||||
setSearchTerm, |
||||
setSelectedRecord, |
||||
showNoResults, |
||||
showSearchResults |
||||
} = useSalesforceLinkDialog(); |
||||
|
||||
const handleChange = useCallback((event: Event) => { |
||||
const value = getFieldValue(event); |
||||
|
||||
setSearchTerm(value); |
||||
}, [ getFieldValue ]); |
||||
|
||||
const handleSubmit = useCallback(() => { |
||||
dispatch(hideDialog()); |
||||
linkMeeting(); |
||||
}, [ hideDialog, linkMeeting ]); |
||||
|
||||
const renderSpinner = () => ( |
||||
<div className = { classes.spinner }> |
||||
<Spinner |
||||
isCompleting = { false } |
||||
size = 'medium' /> |
||||
</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') } |
||||
row = '4' |
||||
value = { notes } /> |
||||
</div> |
||||
); |
||||
|
||||
const renderRecordsSearch = () => !selectedRecord && ( |
||||
<div className = { classes.recordsSearchContainer }> |
||||
<Icon |
||||
className = { classes.searchIcon } |
||||
color = { BaseTheme.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 => ( |
||||
<RecordItem |
||||
key = { `record-${item.id}` } |
||||
/* eslint-disable-next-line react/jsx-no-bind */ |
||||
onClick = { () => setSelectedRecord(item) } |
||||
{ ...item } /> |
||||
))} |
||||
</ul> |
||||
); |
||||
}; |
||||
|
||||
return ( |
||||
<Dialog |
||||
disableEnter = { true } |
||||
disableFooter = { !selectedRecord } |
||||
height = { 'medium' } |
||||
okDisabled = { !selectedRecord } |
||||
okKey = 'dialog.linkMeeting' |
||||
/* eslint-disable-next-line react/jsx-no-bind */ |
||||
onDecline = { () => setSelectedRecord(null) } |
||||
onSubmit = { handleSubmit } |
||||
titleKey = 'dialog.linkMeetingTitle' |
||||
width = { 'small' }> |
||||
<div className = { classes.container } > |
||||
{renderRecordsSearch()} |
||||
{renderContent()} |
||||
</div> |
||||
</Dialog> |
||||
); |
||||
} |
||||
|
||||
export default SalesforceLinkDialog; |
@ -0,0 +1 @@ |
||||
export { default as SalesforceLinkDialog } from './SalesforceLinkDialog'; |
@ -0,0 +1,36 @@ |
||||
import { |
||||
IconRecordAccount, |
||||
IconRecordContact, |
||||
IconRecordLead, |
||||
IconRecordOpportunity |
||||
} from '../base/icons'; |
||||
|
||||
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 = { |
||||
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' |
||||
} |
||||
}; |
@ -0,0 +1,93 @@ |
||||
// @flow
|
||||
|
||||
import { doGetJSON } from '../base/util'; |
||||
|
||||
/** |
||||
* 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: Object |
||||
) { |
||||
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: Object |
||||
) { |
||||
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); |
||||
} |
@ -0,0 +1,2 @@ |
||||
export * from './components'; |
||||
export * from './actions'; |
@ -0,0 +1,146 @@ |
||||
import { useCallback, useEffect, useState } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
|
||||
import { getCurrentConference } from '../base/conference'; |
||||
import { |
||||
hideNotification, |
||||
NOTIFICATION_TIMEOUT_TYPE, |
||||
NOTIFICATION_TYPE, |
||||
SALESFORCE_LINK_NOTIFICATION_ID, |
||||
showNotification |
||||
} from '../notifications'; |
||||
|
||||
import { |
||||
executeLinkMeetingRequest, |
||||
getRecentSessionRecords, |
||||
getSessionRecordDetails, |
||||
searchSessionRecords |
||||
} from './functions'; |
||||
|
||||
export const useSalesforceLinkDialog = () => { |
||||
const { t } = useTranslation(); |
||||
const dispatch = useDispatch(); |
||||
const [ selectedRecord, setSelectedRecord ] = useState(null); |
||||
const [ selectedRecordOwner, setSelectedRecordOwner ] = useState(null); |
||||
const [ records, setRecords ] = useState([]); |
||||
const [ isLoading, setLoading ] = useState(false); |
||||
const [ searchTerm, setSearchTerm ] = useState(null); |
||||
const [ notes, setNotes ] = useState(null); |
||||
const [ hasRecordsErrors, setRecordsErrors ] = useState(false); |
||||
const [ hasDetailsErrors, setDetailsErrors ] = useState(false); |
||||
const conference = useSelector(getCurrentConference); |
||||
const sessionId = conference.getMeetingUniqueId(); |
||||
const { salesforceUrl } = useSelector(state => state['features/base/config']); |
||||
const { jwt } = useSelector(state => 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); |
||||
} |
||||
}; |
||||
|
||||
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) { |
||||
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, |
||||
salesforceUrl, |
||||
selectedRecord, |
||||
showNotification |
||||
]); |
||||
|
||||
return { |
||||
hasDetailsErrors, |
||||
hasRecordsErrors, |
||||
isLoading, |
||||
linkMeeting, |
||||
notes, |
||||
records, |
||||
searchTerm, |
||||
selectedRecord, |
||||
selectedRecordOwner, |
||||
setNotes, |
||||
setSearchTerm, |
||||
setSelectedRecord, |
||||
showNoResults, |
||||
showSearchResults |
||||
}; |
||||
}; |
@ -0,0 +1,46 @@ |
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../../analytics'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { IconSalesforce } from '../../../base/icons'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; |
||||
import { navigate } |
||||
from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef'; |
||||
import { screen } from '../../../mobile/navigation/routes'; |
||||
|
||||
/** |
||||
* Implementation of a button for opening the Salesforce link dialog. |
||||
*/ |
||||
class LinkToSalesforceButton extends AbstractButton<AbstractButtonProps, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce'; |
||||
icon = IconSalesforce; |
||||
label = 'toolbar.linkToSalesforce'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens the Salesforce link dialog. |
||||
* |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_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) { |
||||
return { |
||||
visible: Boolean(state['features/base/config'].salesforceUrl) |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(mapStateToProps)(LinkToSalesforceButton)); |
@ -0,0 +1,45 @@ |
||||
// @flow
|
||||
|
||||
import { createToolbarEvent, sendAnalytics } from '../../../analytics'; |
||||
import { openDialog } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { IconSalesforce } from '../../../base/icons'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { AbstractButton, type AbstractButtonProps } from '../../../base/toolbox/components'; |
||||
import { SalesforceLinkDialog } from '../../../salesforce'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@link LinkToSalesforce}. |
||||
*/ |
||||
type Props = AbstractButtonProps & { |
||||
|
||||
/** |
||||
* The redux {@code dispatch} function. |
||||
*/ |
||||
dispatch: Function |
||||
}; |
||||
|
||||
/** |
||||
* Implementation of a button for opening the Salesforce link dialog. |
||||
*/ |
||||
class LinkToSalesforce extends AbstractButton<Props, *> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.linkToSalesforce'; |
||||
icon = IconSalesforce; |
||||
label = 'toolbar.linkToSalesforce'; |
||||
tooltip = 'toolbar.linkToSalesforce'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens the Salesforce link dialog. |
||||
* |
||||
* @protected |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
const { dispatch } = this.props; |
||||
|
||||
sendAnalytics(createToolbarEvent('link.to.salesforce')); |
||||
dispatch(openDialog(SalesforceLinkDialog)); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect()(LinkToSalesforce)); |
Loading…
Reference in new issue