init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
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

@@ -0,0 +1,186 @@
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

@@ -0,0 +1,148 @@
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

@@ -0,0 +1,107 @@
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: '1rem',
fontWeight: 400,
lineHeight: '1.25rem',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
recordType: {
fontSize: '0.875rem',
fontWeight: 400,
lineHeight: '1.125rem'
}
};
});
/**
* 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

@@ -0,0 +1,284 @@
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 { 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: '1rem',
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,
...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: '1.125rem',
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;