mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
ref(invite): add people form (#13207)
This commit is contained in:
@@ -60,7 +60,6 @@ $flagsImagePath: "../images/";
|
||||
@import 'filmstrip/vertical_filmstrip';
|
||||
@import 'filmstrip/vertical_filmstrip_overrides';
|
||||
@import 'unsupported-browser/main';
|
||||
@import 'modals/invite/add-people';
|
||||
@import 'deep-linking/main';
|
||||
@import 'transcription-subtitles';
|
||||
@import '_meetings_list.scss';
|
||||
|
||||
@@ -12,24 +12,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling inline dialog errors.
|
||||
*/
|
||||
.inline-dialog-error {
|
||||
margin-top: 16px;
|
||||
|
||||
&-text {
|
||||
color: $dialogErrorText;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: block;
|
||||
margin: 16px auto 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styling shared video dialog errors.
|
||||
*/
|
||||
|
||||
@@ -41,10 +41,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Styles errors in the MultiSelectAutocomplete.
|
||||
*/
|
||||
.autocomplete-error {
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
@@ -1,57 +1,4 @@
|
||||
.invite-more {
|
||||
&-container {
|
||||
margin-bottom: 8px;
|
||||
transition: margin-bottom 0.3s;
|
||||
|
||||
&.elevated {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&-header {
|
||||
max-width: 100%;
|
||||
margin-bottom: 16px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&-button {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
height: 40px;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 16px;
|
||||
background: #0376DA;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
&:hover {
|
||||
background: #278ADF;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
margin-left: 8px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
&-dialog {
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
@@ -65,59 +12,6 @@
|
||||
background: #5E6D7A;
|
||||
}
|
||||
|
||||
&.email-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 8px 8px 16px;
|
||||
margin-top: 24px;
|
||||
width: calc(100% - 26px);
|
||||
height: 22px;
|
||||
|
||||
background: #2A3A4B;
|
||||
border: 1px solid #5E6D7A;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.invite-buttons {
|
||||
width: 100%;
|
||||
text-align: right;
|
||||
margin-top: 8px;
|
||||
|
||||
& > a {
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
min-width: 48px;
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&-cancel {
|
||||
margin-right: 16px;
|
||||
padding: 7px 15px;
|
||||
background: #2A3A4B;
|
||||
border: 1px solid #5E6D7A;
|
||||
}
|
||||
|
||||
&-add {
|
||||
padding: 8px 16px;
|
||||
background: #0376DA;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
& > a {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.stream {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -158,14 +52,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-browser {
|
||||
.invite-more-content {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.invite-more-button {
|
||||
height: 48px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { translate } from '../../../i18n/functions';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import Button from '../../../ui/components/web/Button';
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
dialog: {
|
||||
backgroundColor: theme.palette.ui01,
|
||||
border: `1px solid ${theme.palette.ui04}`,
|
||||
borderRadius: `${Number(theme.shape.borderRadius)}px`,
|
||||
boxShadow: '0px 1px 2px rgba(41, 41, 41, 0.25)',
|
||||
color: theme.palette.text01,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
padding: `${theme.spacing(3)} 10`,
|
||||
'& .retry-button': {
|
||||
margin: '16px auto 0 auto'
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link InlineDialogFailure}.
|
||||
*/
|
||||
interface IProps extends WithTranslation {
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Allows to retry the call that previously didn't succeed.
|
||||
@@ -22,50 +40,48 @@ interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* Inline dialog that represents a failure and allows a retry.
|
||||
*
|
||||
* @returns {Element}
|
||||
*/
|
||||
class InlineDialogFailure extends Component<IProps> {
|
||||
/**
|
||||
* Renders the content of this component.
|
||||
*
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const { t, showSupportLink } = this.props;
|
||||
const InlineDialogFailure = ({
|
||||
onRetry,
|
||||
showSupportLink
|
||||
}: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { classes } = useStyles();
|
||||
|
||||
const supportLink = interfaceConfig.SUPPORT_URL;
|
||||
const supportString = t('inlineDialogFailure.supportMsg');
|
||||
const supportLinkElem
|
||||
= supportLink && showSupportLink
|
||||
? (
|
||||
<div className = 'inline-dialog-error-text'>
|
||||
<span>{ supportString.padEnd(supportString.length + 1) }
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
href = { supportLink }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{ t('inlineDialogFailure.support') }
|
||||
</a>
|
||||
</span>
|
||||
<span>.</span>
|
||||
</div>
|
||||
)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div className = 'inline-dialog-error'>
|
||||
<div className = 'inline-dialog-error-text'>
|
||||
{ t('inlineDialogFailure.msg') }
|
||||
</div>
|
||||
{ supportLinkElem }
|
||||
<Button
|
||||
className = 'inline-dialog-error-button'
|
||||
label = { t('inlineDialogFailure.retry') }
|
||||
onClick = { this.props.onRetry } />
|
||||
const supportLink = interfaceConfig.SUPPORT_URL;
|
||||
const supportString = t('inlineDialogFailure.supportMsg');
|
||||
const supportLinkElem = supportLink && showSupportLink
|
||||
? (
|
||||
<div>
|
||||
<span>{ supportString.padEnd(supportString.length + 1) }
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
href = { supportLink }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{ t('inlineDialogFailure.support') }
|
||||
</a>
|
||||
</span>
|
||||
<span>.</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
: null;
|
||||
|
||||
export default translate(InlineDialogFailure);
|
||||
return (
|
||||
<div className = { classes.dialog }>
|
||||
<div>
|
||||
{ t('inlineDialogFailure.msg') }
|
||||
</div>
|
||||
{ supportLinkElem }
|
||||
<Button
|
||||
className = 'retry-button'
|
||||
label = { t('inlineDialogFailure.retry') }
|
||||
onClick = { onRetry } />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InlineDialogFailure;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// @flow
|
||||
|
||||
import AKInlineDialog from '@atlaskit/inline-dialog';
|
||||
import { MultiSelectStateless } from '@atlaskit/multi-select';
|
||||
import _debounce from 'lodash/debounce';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { MultiSelectItem } from '../../../ui/components/types';
|
||||
import MultiSelect from '../../../ui/components/web/MultiSelect';
|
||||
import logger from '../../logger';
|
||||
|
||||
import InlineDialogFailure from './InlineDialogFailure';
|
||||
@@ -13,119 +11,119 @@ import InlineDialogFailure from './InlineDialogFailure';
|
||||
* The type of the React {@code Component} props of
|
||||
* {@link MultiSelectAutocomplete}.
|
||||
*/
|
||||
type Props = {
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* The default value of the selected item.
|
||||
*/
|
||||
defaultValue: Array<Object>,
|
||||
defaultValue?: Array<any>;
|
||||
|
||||
/**
|
||||
* Optional footer to show as a last element in the results.
|
||||
* Should be of type {content: <some content>}.
|
||||
*/
|
||||
footer: Object,
|
||||
footer?: any;
|
||||
|
||||
/**
|
||||
* Indicates if the component is disabled.
|
||||
*/
|
||||
isDisabled: boolean,
|
||||
isDisabled: boolean;
|
||||
|
||||
/**
|
||||
* Text to display while a query is executing.
|
||||
*/
|
||||
loadingMessage: string,
|
||||
loadingMessage: string;
|
||||
|
||||
/**
|
||||
* The text to show when no matches are found.
|
||||
*/
|
||||
noMatchesFound: string,
|
||||
noMatchesFound: string;
|
||||
|
||||
/**
|
||||
* The function called immediately before a selection has been actually
|
||||
* selected. Provides an opportunity to do any formatting.
|
||||
*/
|
||||
onItemSelected: Function,
|
||||
onItemSelected: Function;
|
||||
|
||||
/**
|
||||
* The function called when the selection changes.
|
||||
*/
|
||||
onSelectionChange: Function,
|
||||
onSelectionChange: Function;
|
||||
|
||||
/**
|
||||
* The placeholder text of the input component.
|
||||
*/
|
||||
placeholder: string,
|
||||
placeholder: string;
|
||||
|
||||
/**
|
||||
* The service providing the search.
|
||||
*/
|
||||
resourceClient: { makeQuery: Function, parseResults: Function },
|
||||
resourceClient: { makeQuery: Function; parseResults: Function; };
|
||||
|
||||
/**
|
||||
* Indicates if the component should fit the container.
|
||||
*/
|
||||
shouldFitContainer: boolean,
|
||||
shouldFitContainer: boolean;
|
||||
|
||||
/**
|
||||
* Indicates if we should focus.
|
||||
*/
|
||||
shouldFocus: boolean,
|
||||
shouldFocus: boolean;
|
||||
|
||||
/**
|
||||
* Indicates whether the support link should be shown in case of an error.
|
||||
*/
|
||||
showSupportLink: Boolean,
|
||||
};
|
||||
showSupportLink: Boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} state of
|
||||
* {@link MultiSelectAutocomplete}.
|
||||
*/
|
||||
type State = {
|
||||
|
||||
/**
|
||||
* Indicates if the dropdown is open.
|
||||
*/
|
||||
isOpen: boolean,
|
||||
|
||||
/**
|
||||
* The text that filters the query result of the search.
|
||||
*/
|
||||
filterValue: string,
|
||||
|
||||
/**
|
||||
* Indicates if the component is currently loading results.
|
||||
*/
|
||||
loading: boolean,
|
||||
interface IState {
|
||||
|
||||
/**
|
||||
* Indicates if there was an error.
|
||||
*/
|
||||
error: boolean,
|
||||
error: boolean;
|
||||
|
||||
/**
|
||||
* The text that filters the query result of the search.
|
||||
*/
|
||||
filterValue: string;
|
||||
|
||||
/**
|
||||
* Indicates if the dropdown is open.
|
||||
*/
|
||||
isOpen: boolean;
|
||||
|
||||
/**
|
||||
* The list of result items.
|
||||
*/
|
||||
items: Array<Object>,
|
||||
items: Array<MultiSelectItem>;
|
||||
|
||||
/**
|
||||
* Indicates if the component is currently loading results.
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* The list of selected items.
|
||||
*/
|
||||
selectedItems: Array<Object>
|
||||
};
|
||||
selectedItems: Array<MultiSelectItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
* A MultiSelect that is also auto-completing.
|
||||
*/
|
||||
class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
class MultiSelectAutocomplete extends Component<IProps, IState> {
|
||||
/**
|
||||
* Initializes a new {@code MultiSelectAutocomplete} instance.
|
||||
*
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
const defaultValue = this.props.defaultValue || [];
|
||||
@@ -148,11 +146,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
/**
|
||||
* Sets the items to display as selected.
|
||||
*
|
||||
* @param {Array<Object>} selectedItems - The list of items to display as
|
||||
* @param {Array<MultiSelectItem>} selectedItems - The list of items to display as
|
||||
* having been selected.
|
||||
* @returns {void}
|
||||
*/
|
||||
setSelectedItems(selectedItems: Array<Object> = []) {
|
||||
setSelectedItems(selectedItems: Array<MultiSelectItem> = []) {
|
||||
this.setState({ selectedItems });
|
||||
}
|
||||
|
||||
@@ -162,37 +160,32 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
render() {
|
||||
const shouldFitContainer = this.props.shouldFitContainer || false;
|
||||
const shouldFocus = this.props.shouldFocus || false;
|
||||
const isDisabled = this.props.isDisabled || false;
|
||||
const autoFocus = this.props.shouldFocus || false;
|
||||
const disabled = this.props.isDisabled || false;
|
||||
const placeholder = this.props.placeholder || '';
|
||||
const noMatchesFound = this.props.noMatchesFound || '';
|
||||
const errorDialog = this._renderError();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MultiSelectStateless
|
||||
<MultiSelect
|
||||
autoFocus = { autoFocus }
|
||||
disabled = { disabled }
|
||||
error = { this.state.error }
|
||||
errorDialog = { errorDialog }
|
||||
filterValue = { this.state.filterValue }
|
||||
footer = { this.props.footer }
|
||||
icon = { null }
|
||||
isDisabled = { isDisabled }
|
||||
isLoading = { this.state.loading }
|
||||
isOpen = { this.state.isOpen }
|
||||
items = { this.state.items }
|
||||
loadingMessage = { this.props.loadingMessage }
|
||||
noMatchesFound = { noMatchesFound }
|
||||
noMatchesText = { noMatchesFound }
|
||||
onFilterChange = { this._onFilterChange }
|
||||
onRemoved = { this._onSelectionChange }
|
||||
onSelected = { this._onSelectionChange }
|
||||
placeholder = { placeholder }
|
||||
selectedItems = { this.state.selectedItems }
|
||||
shouldFitContainer = { shouldFitContainer }
|
||||
shouldFocus = { shouldFocus } />
|
||||
{ this._renderError() }
|
||||
selectedItems = { this.state.selectedItems } />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onFilterChange: (string) => void;
|
||||
|
||||
/**
|
||||
* Sets the state and sends a query on filter change.
|
||||
@@ -201,7 +194,7 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onFilterChange(filterValue) {
|
||||
_onFilterChange(filterValue: string) {
|
||||
this.setState({
|
||||
// Clean the error if the filterValue is empty.
|
||||
error: this.state.error && Boolean(filterValue),
|
||||
@@ -215,8 +208,6 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_onRetry: () => void;
|
||||
|
||||
/**
|
||||
* Retries the query on retry.
|
||||
*
|
||||
@@ -227,18 +218,16 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
this._sendQuery(this.state.filterValue);
|
||||
}
|
||||
|
||||
_onSelectionChange: (Object) => void;
|
||||
|
||||
/**
|
||||
* Updates the selected items when a selection event occurs.
|
||||
*
|
||||
* @param {Object} item - The selected item.
|
||||
* @param {any} item - The selected item.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelectionChange(item) {
|
||||
_onSelectionChange(item: any) {
|
||||
const existing
|
||||
= this.state.selectedItems.find(k => k.value === item.value);
|
||||
= this.state.selectedItems.find((k: any) => k.value === item.value);
|
||||
let selectedItems = this.state.selectedItems;
|
||||
|
||||
if (existing) {
|
||||
@@ -265,30 +254,22 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
if (!this.state.error) {
|
||||
return null;
|
||||
}
|
||||
const content = (
|
||||
<div className = 'autocomplete-error'>
|
||||
<InlineDialogFailure
|
||||
onRetry = { this._onRetry }
|
||||
showSupportLink = { this.props.showSupportLink } />
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<AKInlineDialog
|
||||
content = { content }
|
||||
isOpen = { true } />
|
||||
|
||||
<InlineDialogFailure
|
||||
onRetry = { this._onRetry }
|
||||
showSupportLink = { this.props.showSupportLink } />
|
||||
);
|
||||
}
|
||||
|
||||
_sendQuery: (string) => void;
|
||||
|
||||
/**
|
||||
* Sends a query to the resourceClient.
|
||||
*
|
||||
* @param {string} filterValue - The string to use for the search.
|
||||
* @returns {void}
|
||||
*/
|
||||
_sendQuery(filterValue) {
|
||||
_sendQuery(filterValue: string) {
|
||||
if (!filterValue) {
|
||||
return;
|
||||
}
|
||||
@@ -299,11 +280,11 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
|
||||
const resourceClient = this.props.resourceClient || {
|
||||
makeQuery: () => Promise.resolve([]),
|
||||
parseResults: results => results
|
||||
parseResults: (results: any) => results
|
||||
};
|
||||
|
||||
resourceClient.makeQuery(filterValue)
|
||||
.then(results => {
|
||||
.then((results: any) => {
|
||||
if (this.state.filterValue !== filterValue) {
|
||||
this.setState({
|
||||
error: false
|
||||
@@ -311,20 +292,15 @@ class MultiSelectAutocomplete extends Component<Props, State> {
|
||||
|
||||
return;
|
||||
}
|
||||
const itemGroups = [
|
||||
{
|
||||
items: resourceClient.parseResults(results)
|
||||
}
|
||||
];
|
||||
|
||||
this.setState({
|
||||
items: itemGroups,
|
||||
items: resourceClient.parseResults(results),
|
||||
isOpen: true,
|
||||
loading: false,
|
||||
error: false
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
.catch((error: Error) => {
|
||||
logger.error('MultiSelectAutocomplete error in query', error);
|
||||
|
||||
this.setState({
|
||||
@@ -105,3 +105,11 @@ export interface ISwitchProps {
|
||||
*/
|
||||
onChange: (on?: boolean) => void;
|
||||
}
|
||||
|
||||
export type MultiSelectItem = {
|
||||
content: string;
|
||||
description?: string;
|
||||
elemBefore?: Element;
|
||||
isDisabled?: boolean;
|
||||
value: string;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,8 @@ interface IProps extends IInputProps {
|
||||
maxRows?: number;
|
||||
minRows?: number;
|
||||
name?: string;
|
||||
onBlur?: (e: any) => void;
|
||||
onFocus?: (event: React.FocusEvent) => void;
|
||||
onKeyPress?: (e: React.KeyboardEvent) => void;
|
||||
readOnly?: boolean;
|
||||
required?: boolean;
|
||||
@@ -148,7 +150,9 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
maxRows,
|
||||
minRows,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onKeyPress,
|
||||
placeholder,
|
||||
readOnly = false,
|
||||
@@ -208,7 +212,9 @@ const Input = React.forwardRef<any, IProps>(({
|
||||
{ ...(id ? { id } : {}) }
|
||||
maxLength = { maxLength }
|
||||
name = { name }
|
||||
onBlur = { onBlur }
|
||||
onChange = { handleChange }
|
||||
onFocus = { onFocus }
|
||||
onKeyPress = { onKeyPress }
|
||||
placeholder = { placeholder }
|
||||
readOnly = { readOnly }
|
||||
|
||||
175
react/features/base/ui/components/web/MultiSelect.tsx
Normal file
175
react/features/base/ui/components/web/MultiSelect.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { makeStyles } from 'tss-react/mui';
|
||||
|
||||
import { IconCloseLarge } from '../../../icons/svg';
|
||||
import { withPixelLineHeight } from '../../../styles/functions.web';
|
||||
import { MultiSelectItem } from '../types';
|
||||
|
||||
import ClickableIcon from './ClickableIcon';
|
||||
import Input from './Input';
|
||||
|
||||
interface IProps {
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
errorDialog?: JSX.Element | null;
|
||||
filterValue?: string;
|
||||
isOpen?: boolean;
|
||||
items: MultiSelectItem[];
|
||||
noMatchesText?: string;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onRemoved: (item: any) => void;
|
||||
onSelected: (item: any) => void;
|
||||
placeholder?: string;
|
||||
selectedItems?: MultiSelectItem[];
|
||||
}
|
||||
|
||||
const useStyles = makeStyles()(theme => {
|
||||
return {
|
||||
container: {
|
||||
position: 'relative'
|
||||
},
|
||||
items: {
|
||||
'&.found': {
|
||||
position: 'absolute',
|
||||
boxShadow: '0px 5px 10px rgba(0, 0, 0, 0.75)'
|
||||
},
|
||||
marginTop: theme.spacing(2),
|
||||
width: '100%',
|
||||
backgroundColor: theme.palette.ui01,
|
||||
border: `1px solid ${theme.palette.ui04}`,
|
||||
borderRadius: `${Number(theme.shape.borderRadius)}px`,
|
||||
...withPixelLineHeight(theme.typography.bodyShortRegular),
|
||||
zIndex: 2,
|
||||
maxHeight: '400px',
|
||||
overflowY: 'auto',
|
||||
padding: '0'
|
||||
},
|
||||
listItem: {
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
padding: `${theme.spacing(2)} ${theme.spacing(3)}`,
|
||||
alignItems: 'center',
|
||||
'& .content': {
|
||||
// 38px because of the icon before the content
|
||||
inlineSize: 'calc(100% - 38px)',
|
||||
overflowWrap: 'break-word',
|
||||
marginLeft: theme.spacing(2),
|
||||
color: theme.palette.text01,
|
||||
'&.with-remove': {
|
||||
// 60px because of the icon before the content and the remove button
|
||||
inlineSize: 'calc(100% - 60px)',
|
||||
marginRight: theme.spacing(2),
|
||||
'&.without-before': {
|
||||
marginLeft: 0,
|
||||
inlineSize: 'calc(100% - 38px)'
|
||||
}
|
||||
},
|
||||
'&.without-before': {
|
||||
marginLeft: 0,
|
||||
inlineSize: '100%'
|
||||
}
|
||||
},
|
||||
'&.found': {
|
||||
cursor: 'pointer',
|
||||
padding: `10px ${theme.spacing(3)}`,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui02
|
||||
}
|
||||
},
|
||||
'&.disabled': {
|
||||
cursor: 'not-allowed',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.ui01
|
||||
},
|
||||
color: theme.palette.text03
|
||||
}
|
||||
},
|
||||
errorMessage: {
|
||||
position: 'absolute',
|
||||
marginTop: theme.spacing(2),
|
||||
width: '100%'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
const MultiSelect = ({
|
||||
autoFocus,
|
||||
disabled,
|
||||
error,
|
||||
errorDialog,
|
||||
placeholder,
|
||||
items,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
isOpen,
|
||||
noMatchesText,
|
||||
onSelected,
|
||||
selectedItems,
|
||||
onRemoved
|
||||
}: IProps) => {
|
||||
const { classes } = useStyles();
|
||||
const inputRef = useRef();
|
||||
const selectItem = useCallback(item => () => onSelected(item), [ onSelected ]);
|
||||
const removeItem = useCallback(item => () => onRemoved(item), [ onRemoved ]);
|
||||
const foundItems = useMemo(() => (
|
||||
<div className = { `${classes.items} found` }>
|
||||
{
|
||||
items.length > 0
|
||||
? items.map(item => (
|
||||
<div
|
||||
className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''} found` }
|
||||
key = { item.value }
|
||||
onClick = { item.isDisabled ? undefined : selectItem(item) }>
|
||||
{item.elemBefore}
|
||||
<div className = { `content ${item.elemBefore ? '' : 'without-before'}` }>
|
||||
{item.content}
|
||||
{item.description && <p>{item.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
: <div>{noMatchesText}</div>
|
||||
}
|
||||
</div>
|
||||
), [ items ]);
|
||||
|
||||
const errorMessageDialog = useMemo(() =>
|
||||
error && <div className = { classes.errorMessage }>
|
||||
{ errorDialog }
|
||||
</div>, [ error ]);
|
||||
|
||||
return (
|
||||
<div className = { classes.container }>
|
||||
<Input
|
||||
autoFocus = { autoFocus }
|
||||
disabled = { disabled }
|
||||
onChange = { onFilterChange }
|
||||
placeholder = { placeholder }
|
||||
ref = { inputRef }
|
||||
value = { filterValue ?? '' } />
|
||||
{isOpen && foundItems}
|
||||
{ errorMessageDialog }
|
||||
{ selectedItems && selectedItems?.length > 0 && (
|
||||
<div className = { classes.items }>
|
||||
{ selectedItems.map(item => (
|
||||
<div
|
||||
className = { `${classes.listItem} ${item.isDisabled ? 'disabled' : ''}` }
|
||||
key = { item.value }>
|
||||
{item.elemBefore}
|
||||
<div className = { `content with-remove ${item.elemBefore ? '' : 'without-before'}` }>
|
||||
<p>{item.content}</p>
|
||||
</div>
|
||||
<ClickableIcon
|
||||
accessibilityLabel = { 'multi-select-unselect' }
|
||||
icon = { IconCloseLarge }
|
||||
id = 'modal-header-close-button'
|
||||
onClick = { removeItem(item) } />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
@@ -3,13 +3,12 @@ import { Component } from 'react';
|
||||
import { createInviteDialogEvent } from '../../../analytics/AnalyticsEvents';
|
||||
import { sendAnalytics } from '../../../analytics/functions';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { showNotification } from '../../../notifications/actions';
|
||||
import { showErrorNotification, showNotification } from '../../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../../notifications/constants';
|
||||
import { INotificationProps } from '../../../notifications/types';
|
||||
import { invite } from '../../actions';
|
||||
import { INVITE_TYPES } from '../../constants';
|
||||
import {
|
||||
GetInviteResultsOptions,
|
||||
getInviteResultsForQuery,
|
||||
getInviteTypeCounts,
|
||||
isAddPeopleEnabled,
|
||||
@@ -17,7 +16,7 @@ import {
|
||||
isSipInviteEnabled
|
||||
} from '../../functions';
|
||||
import logger from '../../logger';
|
||||
import { IInvitee } from '../../types';
|
||||
import { IInviteSelectItem, IInvitee } from '../../types';
|
||||
|
||||
export interface IProps {
|
||||
|
||||
@@ -26,6 +25,9 @@ export interface IProps {
|
||||
*/
|
||||
_addPeopleEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The app id of the user.
|
||||
*/
|
||||
_appId: string;
|
||||
|
||||
/**
|
||||
@@ -48,17 +50,17 @@ export interface IProps {
|
||||
*/
|
||||
_dialOutRegionUrl: string;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The JWT token.
|
||||
*/
|
||||
_jwt: string;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The query types used when searching people.
|
||||
*/
|
||||
_peopleSearchQueryTypes: Array<string>;
|
||||
|
||||
/**
|
||||
/**
|
||||
* The URL pointing to the service allowing for people search.
|
||||
*/
|
||||
_peopleSearchUrl: string;
|
||||
@@ -66,7 +68,7 @@ export interface IProps {
|
||||
/**
|
||||
* Whether or not to allow sip invites.
|
||||
*/
|
||||
_sipInviteEnabled: boolean;
|
||||
_sipInviteEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The Redux dispatch function.
|
||||
@@ -90,14 +92,13 @@ export interface IState {
|
||||
/**
|
||||
* The list of invite items.
|
||||
*/
|
||||
inviteItems: Array<Object>;
|
||||
inviteItems: Array<IInviteSelectItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements an abstract dialog to invite people to the conference.
|
||||
*/
|
||||
export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
extends Component<P, S> {
|
||||
export default class AbstractAddPeopleDialog<P extends IProps, S extends IState> extends Component<P, S> {
|
||||
/**
|
||||
* Constructor of the component.
|
||||
*
|
||||
@@ -112,7 +113,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
/**
|
||||
* Retrieves the notification display name for the invitee.
|
||||
*
|
||||
* @param {Object} invitee - The invitee object.
|
||||
* @param {IInvitee} invitee - The invitee object.
|
||||
* @returns {string}
|
||||
*/
|
||||
_getDisplayName(invitee: IInvitee) {
|
||||
@@ -124,7 +125,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
return invitee.address;
|
||||
}
|
||||
|
||||
return invitee.name;
|
||||
return invitee.name ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,8 +136,8 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
* no invites left to send. If any are left, that means an invite failed
|
||||
* and an error state should display.
|
||||
*
|
||||
* @param {Array<Object>} invitees - The items to be invited.
|
||||
* @returns {Promise<Array<Object>>}
|
||||
* @param {Array<IInvitee>} invitees - The items to be invited.
|
||||
* @returns {Promise<Array<any>>}
|
||||
*/
|
||||
_invite(invitees: IInvitee[]) {
|
||||
const inviteTypeCounts = getInviteTypeCounts(invitees);
|
||||
@@ -176,10 +177,9 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
'error', 'invite', {
|
||||
...erroredInviteTypeCounts
|
||||
}));
|
||||
|
||||
this.setState({
|
||||
addToCallError: true
|
||||
});
|
||||
dispatch(showErrorNotification({
|
||||
titleKey: 'addPeople.failedToAdd'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
} else if (!_callFlowsEnabled) {
|
||||
const invitedCount = invitees.length;
|
||||
let notificationProps: INotificationProps | undefined;
|
||||
@@ -188,7 +188,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
notificationProps = {
|
||||
titleArguments: {
|
||||
name: this._getDisplayName(invitees[0]),
|
||||
count: invitedCount - 1
|
||||
count: `${invitedCount - 1}`
|
||||
},
|
||||
titleKey: 'notify.invitedThreePlusMembers'
|
||||
};
|
||||
@@ -250,7 +250,7 @@ export default class AbstractAddPeopleDialog<P extends IProps, S extends IState>
|
||||
_peopleSearchUrl: peopleSearchUrl,
|
||||
_sipInviteEnabled: sipInviteEnabled
|
||||
} = this.props;
|
||||
const options: GetInviteResultsOptions = {
|
||||
const options = {
|
||||
addPeopleEnabled,
|
||||
appId,
|
||||
dialOutAuthUrl,
|
||||
@@ -292,14 +292,14 @@ export function _mapStateToProps(state: IReduxState) {
|
||||
|
||||
return {
|
||||
_addPeopleEnabled: isAddPeopleEnabled(state),
|
||||
_appId: state['features/base/jwt']?.tenant,
|
||||
_callFlowsEnabled: callFlowsEnabled,
|
||||
_dialOutAuthUrl: dialOutAuthUrl,
|
||||
_dialOutRegionUrl: dialOutRegionUrl,
|
||||
_appId: state['features/base/jwt']?.tenant ?? '',
|
||||
_callFlowsEnabled: callFlowsEnabled ?? false,
|
||||
_dialOutAuthUrl: dialOutAuthUrl ?? '',
|
||||
_dialOutRegionUrl: dialOutRegionUrl ?? '',
|
||||
_dialOutEnabled: isDialOutEnabled(state),
|
||||
_jwt: state['features/base/jwt'].jwt,
|
||||
_peopleSearchQueryTypes: peopleSearchQueryTypes,
|
||||
_peopleSearchUrl: peopleSearchUrl,
|
||||
_jwt: state['features/base/jwt'].jwt ?? '',
|
||||
_peopleSearchQueryTypes: peopleSearchQueryTypes ?? [],
|
||||
_peopleSearchUrl: peopleSearchUrl ?? '',
|
||||
_sipInviteEnabled: isSipInviteEnabled(state)
|
||||
};
|
||||
}
|
||||
@@ -27,8 +27,6 @@ import CopyMeetingLinkSection from './CopyMeetingLinkSection';
|
||||
import DialInLimit from './DialInLimit';
|
||||
import DialInSection from './DialInSection';
|
||||
import InviteByEmailSection from './InviteByEmailSection';
|
||||
// eslint-disable-next-line lines-around-comment
|
||||
// @ts-ignore
|
||||
import InviteContactsSection from './InviteContactsSection';
|
||||
import LiveStreamSection from './LiveStreamSection';
|
||||
|
||||
|
||||
@@ -1,63 +1,93 @@
|
||||
// @flow
|
||||
|
||||
import InlineMessage from '@atlaskit/inline-message';
|
||||
import { Theme } from '@mui/material';
|
||||
import { withStyles } from '@mui/styles';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
|
||||
import { IReduxState, IStore } from '../../../../app/types';
|
||||
import Avatar from '../../../../base/avatar/components/Avatar';
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import Icon from '../../../../base/icons/components/Icon';
|
||||
import { IconPhoneRinging } from '../../../../base/icons/svg';
|
||||
import MultiSelectAutocomplete from '../../../../base/react/components/web/MultiSelectAutocomplete';
|
||||
import Button from '../../../../base/ui/components/web/Button';
|
||||
import { BUTTON_TYPES } from '../../../../base/ui/constants.any';
|
||||
import { isVpaasMeeting } from '../../../../jaas/functions';
|
||||
import { hideAddPeopleDialog } from '../../../actions';
|
||||
import { hideAddPeopleDialog } from '../../../actions.web';
|
||||
import { INVITE_TYPES } from '../../../constants';
|
||||
import { IInviteSelectItem, IInvitee } from '../../../types';
|
||||
import AbstractAddPeopleDialog, {
|
||||
type Props as AbstractProps,
|
||||
type State,
|
||||
IProps as AbstractProps,
|
||||
IState,
|
||||
_mapStateToProps as _abstractMapStateToProps
|
||||
} from '../AbstractAddPeopleDialog';
|
||||
|
||||
declare var interfaceConfig: Object;
|
||||
const styles = (theme: Theme) => {
|
||||
return {
|
||||
formWrap: {
|
||||
marginTop: theme.spacing(2)
|
||||
},
|
||||
inviteButtons: {
|
||||
display: 'flex',
|
||||
justifyContent: 'end',
|
||||
marginTop: theme.spacing(2),
|
||||
'& .invite-button': {
|
||||
marginLeft: theme.spacing(2)
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type Props = AbstractProps & {
|
||||
|
||||
interface IProps extends AbstractProps {
|
||||
|
||||
/**
|
||||
* The {@link JitsiMeetConference} which will be used to invite "room" participants.
|
||||
*/
|
||||
_conference: Object,
|
||||
_conference?: Object;
|
||||
|
||||
/**
|
||||
* Whether the meeting belongs to JaaS user.
|
||||
*/
|
||||
_isVpaas: boolean,
|
||||
_isVpaas?: boolean;
|
||||
|
||||
/**
|
||||
* Css classes.
|
||||
*/
|
||||
classes: any;
|
||||
|
||||
/**
|
||||
* The redux {@code dispatch} function.
|
||||
*/
|
||||
dispatch: Dispatch<any>,
|
||||
dispatch: IStore['dispatch'];
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function,
|
||||
};
|
||||
t: Function;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form that enables inviting others to the call.
|
||||
*/
|
||||
class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
_multiselect = null;
|
||||
class InviteContactsForm extends AbstractAddPeopleDialog<IProps, IState> {
|
||||
_multiselect: MultiSelectAutocomplete | null = null;
|
||||
|
||||
_resourceClient: Object;
|
||||
_resourceClient: {
|
||||
makeQuery: (query: string) => Promise<Array<any>>;
|
||||
parseResults: Function;
|
||||
};
|
||||
|
||||
_translations: Object;
|
||||
_translations: {
|
||||
[key: string]: string;
|
||||
_addPeopleEnabled: string;
|
||||
_dialOutEnabled: string;
|
||||
_sipInviteEnabled: string;
|
||||
};
|
||||
|
||||
state = {
|
||||
addToCallError: false,
|
||||
addToCallInProgress: false,
|
||||
inviteItems: []
|
||||
inviteItems: [] as IInviteSelectItem[]
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -66,7 +96,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
* @param {Object} props - The read-only properties with which the new
|
||||
* instance is to be initialized.
|
||||
*/
|
||||
constructor(props: Props) {
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
// Bind event handlers so they are only bound once per instance.
|
||||
@@ -99,11 +129,11 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
/**
|
||||
* React Component method that executes once component is updated.
|
||||
*
|
||||
* @param {Object} prevProps - The state object before the update.
|
||||
* @param {Object} prevState - The state object before the update.
|
||||
* @param {Props} prevProps - The props object before the update.
|
||||
* @param {State} prevState - The state object before the update.
|
||||
* @returns {void}
|
||||
*/
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
componentDidUpdate(prevProps: IProps, prevState: IState) {
|
||||
/**
|
||||
* Clears selected items from the multi select component on successful
|
||||
* invite.
|
||||
@@ -133,7 +163,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
const loadingMessage = 'addPeople.searching';
|
||||
const noMatches = 'addPeople.noResults';
|
||||
|
||||
const features = {
|
||||
const features: { [key: string]: boolean; } = {
|
||||
_dialOutEnabled,
|
||||
_addPeopleEnabled,
|
||||
_sipInviteEnabled
|
||||
@@ -152,9 +182,8 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
|
||||
return (
|
||||
<div
|
||||
className = 'add-people-form-wrap'
|
||||
className = { this.props.classes.formWrap }
|
||||
onKeyDown = { this._onKeyDown }>
|
||||
{ this._renderErrorMessage() }
|
||||
<MultiSelectAutocomplete
|
||||
isDisabled = { isMultiSelectDisabled }
|
||||
loadingMessage = { t(loadingMessage) }
|
||||
@@ -172,21 +201,17 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
_invite: Array<Object> => Promise<*>;
|
||||
|
||||
_isAddDisabled: () => boolean;
|
||||
|
||||
_onItemSelected: (Object) => Object;
|
||||
|
||||
/**
|
||||
* Callback invoked when a selection has been made but before it has been
|
||||
* set as selected.
|
||||
*
|
||||
* @param {Object} item - The item that has just been selected.
|
||||
* @param {IInviteSelectItem} item - The item that has just been selected.
|
||||
* @private
|
||||
* @returns {Object} The item to display as selected in the input.
|
||||
*/
|
||||
_onItemSelected(item) {
|
||||
_onItemSelected(item: IInviteSelectItem) {
|
||||
if (item.item.type === INVITE_TYPES.PHONE) {
|
||||
item.content = item.item.number;
|
||||
}
|
||||
@@ -194,22 +219,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
return item;
|
||||
}
|
||||
|
||||
_onSelectionChange: (Map<*, *>) => void;
|
||||
|
||||
/**
|
||||
* Handles a selection change.
|
||||
*
|
||||
* @param {Array} selectedItems - The list of selected items.
|
||||
* @param {Array<IInviteSelectItem>} selectedItems - The list of selected items.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSelectionChange(selectedItems) {
|
||||
_onSelectionChange(selectedItems: IInviteSelectItem[]) {
|
||||
this.setState({
|
||||
inviteItems: selectedItems
|
||||
});
|
||||
}
|
||||
|
||||
_onSubmit: () => void;
|
||||
|
||||
/**
|
||||
* Submits the selection for inviting.
|
||||
@@ -222,49 +244,43 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
const invitees = inviteItems.map(({ item }) => item);
|
||||
|
||||
this._invite(invitees)
|
||||
.then(invitesLeftToSend => {
|
||||
.then((invitesLeftToSend: IInvitee[]) => {
|
||||
if (invitesLeftToSend.length) {
|
||||
const unsentInviteIDs
|
||||
= invitesLeftToSend.map(invitee =>
|
||||
invitee.id || invitee.user_id || invitee.number);
|
||||
const itemsToSelect
|
||||
= inviteItems.filter(({ item }) =>
|
||||
unsentInviteIDs.includes(item.id || item.user_id || item.number));
|
||||
const itemsToSelect = inviteItems.filter(({ item }) =>
|
||||
unsentInviteIDs.includes(item.id || item.user_id || item.number));
|
||||
|
||||
if (this._multiselect) {
|
||||
this._multiselect.setSelectedItems(itemsToSelect);
|
||||
}
|
||||
} else {
|
||||
this.props.dispatch(hideAddPeopleDialog());
|
||||
}
|
||||
});
|
||||
})
|
||||
.finally(() => this.props.dispatch(hideAddPeopleDialog()));
|
||||
}
|
||||
|
||||
_onSubmitKeyPress: (Object) => void;
|
||||
|
||||
/**
|
||||
* KeyPress handler for accessibility.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @param {KeyboardEvent} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onSubmitKeyPress(e) {
|
||||
_onSubmitKeyPress(e: React.KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
_onKeyDown: (Object) => void;
|
||||
|
||||
/**
|
||||
* Handles 'Enter' key in the form to trigger the invite.
|
||||
*
|
||||
* @param {Object} event - The key event.
|
||||
* @param {KeyboardEvent} event - The key event.
|
||||
* @returns {void}
|
||||
*/
|
||||
_onKeyDown(event) {
|
||||
_onKeyDown(event: React.KeyboardEvent) {
|
||||
const { inviteItems } = this.state;
|
||||
|
||||
if (event.key === 'Enter') {
|
||||
@@ -275,20 +291,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
_parseQueryResults: (?Array<Object>) => Array<Object>;
|
||||
|
||||
/**
|
||||
* Returns the avatar component for a user.
|
||||
*
|
||||
* @param {Object} user - The user.
|
||||
* @param {any} user - The user.
|
||||
* @param {string} className - The CSS class for the avatar component.
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_getAvatar(user, className = 'avatar-small') {
|
||||
_getAvatar(user: any, className = 'avatar-small') {
|
||||
return (
|
||||
<Avatar
|
||||
className = { className }
|
||||
size = { 32 }
|
||||
status = { user.status }
|
||||
url = { user.avatar } />
|
||||
);
|
||||
@@ -305,7 +320,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
* @returns {Object[]} Configuration objects for items to display in the
|
||||
* search autocomplete.
|
||||
*/
|
||||
_parseQueryResults(response = []) {
|
||||
_parseQueryResults(response: IInvitee[] = []) {
|
||||
const { t, _dialOutEnabled } = this.props;
|
||||
|
||||
const userTypes = [ INVITE_TYPES.USER, INVITE_TYPES.VIDEO_ROOM, INVITE_TYPES.ROOM ];
|
||||
@@ -394,10 +409,6 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
];
|
||||
}
|
||||
|
||||
_query: (string) => Promise<Array<Object>>;
|
||||
|
||||
_onClearItems: () => void;
|
||||
|
||||
/**
|
||||
* Clears the selected items from state and form.
|
||||
*
|
||||
@@ -410,16 +421,14 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
this.setState({ inviteItems: [] });
|
||||
}
|
||||
|
||||
_onClearItemsKeyPress: () => void;
|
||||
|
||||
/**
|
||||
* Clears the selected items from state and form.
|
||||
*
|
||||
* @param {Object} e - The key event to handle.
|
||||
* @param {KeyboardEvent} e - The key event to handle.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
_onClearItemsKeyPress(e) {
|
||||
_onClearItemsKeyPress(e: KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this._onClearItems();
|
||||
@@ -433,76 +442,30 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
*/
|
||||
_renderFormActions() {
|
||||
const { inviteItems } = this.state;
|
||||
const { t } = this.props;
|
||||
const { t, classes } = this.props;
|
||||
|
||||
if (!inviteItems.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className = { `invite-more-dialog invite-buttons${this._isAddDisabled() ? ' disabled' : ''}` }>
|
||||
<a
|
||||
<div className = { classes.inviteButtons }>
|
||||
<Button
|
||||
aria-label = { t('dialog.Cancel') }
|
||||
className = 'invite-more-dialog invite-buttons-cancel'
|
||||
className = 'invite-button'
|
||||
label = { t('dialog.Cancel') }
|
||||
onClick = { this._onClearItems }
|
||||
onKeyPress = { this._onClearItemsKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{t('dialog.Cancel')}
|
||||
</a>
|
||||
<a
|
||||
type = { BUTTON_TYPES.SECONDARY } />
|
||||
<Button
|
||||
aria-label = { t('addPeople.add') }
|
||||
className = 'invite-more-dialog invite-buttons-add'
|
||||
className = 'invite-button'
|
||||
disabled = { this._isAddDisabled() }
|
||||
label = { t('addPeople.add') }
|
||||
onClick = { this._onSubmit }
|
||||
onKeyPress = { this._onSubmitKeyPress }
|
||||
role = 'button'
|
||||
tabIndex = { 0 }>
|
||||
{t('addPeople.add')}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the error message if the add doesn't succeed.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement|null}
|
||||
*/
|
||||
_renderErrorMessage() {
|
||||
if (!this.state.addToCallError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { t } = this.props;
|
||||
const supportString = t('inlineDialogFailure.supportMsg');
|
||||
const supportLink = interfaceConfig.SUPPORT_URL;
|
||||
|
||||
const supportLinkContent = supportLink ? (
|
||||
<span>
|
||||
<span>
|
||||
{ supportString.padEnd(supportString.length + 1) }
|
||||
</span>
|
||||
<span>
|
||||
<a
|
||||
aria-label = { supportLink }
|
||||
href = { supportLink }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{ t('inlineDialogFailure.support') }
|
||||
</a>
|
||||
</span>
|
||||
<span>.</span>
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className = 'modal-dialog-form-error'>
|
||||
<InlineMessage
|
||||
title = { t('addPeople.failedToAdd') }
|
||||
type = 'error'>
|
||||
{ supportLinkContent }
|
||||
</InlineMessage>
|
||||
role = 'button' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -515,23 +478,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
*/
|
||||
_renderTelephoneIcon() {
|
||||
return (
|
||||
<span className = 'add-telephone-icon'>
|
||||
<Icon src = { IconPhoneRinging } />
|
||||
</span>
|
||||
<Icon src = { IconPhoneRinging } />
|
||||
);
|
||||
}
|
||||
|
||||
_setMultiSelectElement: (React$ElementRef<*> | null) => void;
|
||||
|
||||
/**
|
||||
* Sets the instance variable for the multi select component
|
||||
* element so it can be accessed directly.
|
||||
*
|
||||
* @param {Object} element - The DOM element for the component's dialog.
|
||||
* @param {MultiSelectAutocomplete} element - The DOM element for the component's dialog.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
_setMultiSelectElement(element) {
|
||||
_setMultiSelectElement(element: MultiSelectAutocomplete) {
|
||||
this._multiselect = element;
|
||||
}
|
||||
}
|
||||
@@ -540,15 +499,15 @@ class InviteContactsForm extends AbstractAddPeopleDialog<Props, State> {
|
||||
* Maps (parts of) the Redux state to the associated
|
||||
* {@code AddPeopleDialog}'s props.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @private
|
||||
* @returns {Props}
|
||||
*/
|
||||
function _mapStateToProps(state) {
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
..._abstractMapStateToProps(state),
|
||||
_isVpaas: isVpaasMeeting(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(InviteContactsForm));
|
||||
export default translate(connect(_mapStateToProps)(withStyles(styles)(InviteContactsForm)));
|
||||
@@ -1,25 +1,16 @@
|
||||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { translate } from '../../../../base/i18n/functions';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import InviteContactsForm from './InviteContactsForm';
|
||||
|
||||
type Props = {
|
||||
|
||||
/**
|
||||
* Invoked to obtain translated strings.
|
||||
*/
|
||||
t: Function
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that represents the invitation section of the {@code AddPeopleDialog}.
|
||||
*
|
||||
* @returns {ReactElement$<any>}
|
||||
*/
|
||||
function InviteContactsSection({ t }: Props) {
|
||||
function InviteContactsSection() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{t('addPeople.addContacts')}</span>
|
||||
@@ -29,4 +20,4 @@ function InviteContactsSection({ t }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default translate(InviteContactsSection);
|
||||
export default InviteContactsSection;
|
||||
@@ -1,6 +1,20 @@
|
||||
import { MultiSelectItem } from '../base/ui/components/types';
|
||||
|
||||
export interface IInvitee {
|
||||
address: string;
|
||||
name: string;
|
||||
allowed?: boolean;
|
||||
id?: string;
|
||||
name?: string;
|
||||
number: string;
|
||||
originalEntry?: string;
|
||||
phone?: string;
|
||||
showCountryCodeReminder?: boolean;
|
||||
type: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
export interface IInviteSelectItem extends MultiSelectItem {
|
||||
filterValues?: string[];
|
||||
item: IInvitee;
|
||||
tag?: any;
|
||||
}
|
||||
|
||||
@@ -40,4 +40,4 @@ export const NOTIFY_CLICK_MODE = {
|
||||
};
|
||||
|
||||
// Around 300 to be displayed above components like chat
|
||||
export const ZINDEX_DIALOG_PORTAL = 300;
|
||||
export const ZINDEX_DIALOG_PORTAL = 302;
|
||||
|
||||
Reference in New Issue
Block a user