From aec86cecc079106aca191f53d4140a9d45d9ccb7 Mon Sep 17 00:00:00 2001 From: Gabriel Borlea Date: Thu, 20 Apr 2023 14:00:42 +0300 Subject: [PATCH] ref(invite): add people form (#13207) --- css/main.scss | 1 - css/modals/_dialog.scss | 18 -- css/modals/invite/_add-people.scss | 7 - css/modals/invite/_invite_more.scss | 117 --------- .../components/web/InlineDialogFailure.tsx | 110 +++++---- ...omplete.js => MultiSelectAutocomplete.tsx} | 150 +++++------- react/features/base/ui/components/types.ts | 8 + .../features/base/ui/components/web/Input.tsx | 6 + .../base/ui/components/web/MultiSelect.tsx | 175 ++++++++++++++ ...eDialog.ts => AbstractAddPeopleDialog.tsx} | 54 ++--- .../add-people-dialog/web/AddPeopleDialog.tsx | 2 - ...ContactsForm.js => InviteContactsForm.tsx} | 223 +++++++----------- ...tsSection.js => InviteContactsSection.tsx} | 19 +- react/features/invite/types.ts | 16 +- react/features/toolbox/constants.ts | 2 +- 15 files changed, 454 insertions(+), 454 deletions(-) rename react/features/base/react/components/web/{MultiSelectAutocomplete.js => MultiSelectAutocomplete.tsx} (70%) create mode 100644 react/features/base/ui/components/web/MultiSelect.tsx rename react/features/invite/components/add-people-dialog/{AbstractAddPeopleDialog.ts => AbstractAddPeopleDialog.tsx} (87%) rename react/features/invite/components/add-people-dialog/web/{InviteContactsForm.js => InviteContactsForm.tsx} (72%) rename react/features/invite/components/add-people-dialog/web/{InviteContactsSection.js => InviteContactsSection.tsx} (60%) diff --git a/css/main.scss b/css/main.scss index 8fdbd51cf0..8c522b2085 100644 --- a/css/main.scss +++ b/css/main.scss @@ -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'; diff --git a/css/modals/_dialog.scss b/css/modals/_dialog.scss index dc1fc2f301..a5f0c5b26c 100644 --- a/css/modals/_dialog.scss +++ b/css/modals/_dialog.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. */ diff --git a/css/modals/invite/_add-people.scss b/css/modals/invite/_add-people.scss index ea09c367f6..bb2d0ee556 100644 --- a/css/modals/invite/_add-people.scss +++ b/css/modals/invite/_add-people.scss @@ -41,10 +41,3 @@ } } } - -/** - * Styles errors in the MultiSelectAutocomplete. - */ -.autocomplete-error { - min-width: 260px; -} diff --git a/css/modals/invite/_invite_more.scss b/css/modals/invite/_invite_more.scss index b09b4d6133..4a77f6dcd2 100644 --- a/css/modals/invite/_invite_more.scss +++ b/css/modals/invite/_invite_more.scss @@ -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; - } -} diff --git a/react/features/base/react/components/web/InlineDialogFailure.tsx b/react/features/base/react/components/web/InlineDialogFailure.tsx index 7245ecbf4e..ac802c2082 100644 --- a/react/features/base/react/components/web/InlineDialogFailure.tsx +++ b/react/features/base/react/components/web/InlineDialogFailure.tsx @@ -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 { - /** - * 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 - ? ( -
- { supportString.padEnd(supportString.length + 1) } - - - - { t('inlineDialogFailure.support') } - - - . -
- ) - : null; - - return ( -
-
- { t('inlineDialogFailure.msg') } -
- { supportLinkElem } -
+ ); +}; + +export default InlineDialogFailure; diff --git a/react/features/base/react/components/web/MultiSelectAutocomplete.js b/react/features/base/react/components/web/MultiSelectAutocomplete.tsx similarity index 70% rename from react/features/base/react/components/web/MultiSelectAutocomplete.js rename to react/features/base/react/components/web/MultiSelectAutocomplete.tsx index ce1b8fd063..2d2637e1ec 100644 --- a/react/features/base/react/components/web/MultiSelectAutocomplete.js +++ b/react/features/base/react/components/web/MultiSelectAutocomplete.tsx @@ -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, + defaultValue?: Array; /** * Optional footer to show as a last element in the results. * Should be of type {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, + items: Array; + + /** + * Indicates if the component is currently loading results. + */ + loading: boolean; /** * The list of selected items. */ - selectedItems: Array -}; + selectedItems: Array; +} /** * A MultiSelect that is also auto-completing. */ -class MultiSelectAutocomplete extends Component { +class MultiSelectAutocomplete extends Component { /** * 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 { /** * Sets the items to display as selected. * - * @param {Array} selectedItems - The list of items to display as + * @param {Array} selectedItems - The list of items to display as * having been selected. * @returns {void} */ - setSelectedItems(selectedItems: Array = []) { + setSelectedItems(selectedItems: Array = []) { this.setState({ selectedItems }); } @@ -162,37 +160,32 @@ class MultiSelectAutocomplete extends Component { * @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 (
- - { this._renderError() } + selectedItems = { this.state.selectedItems } />
); } - _onFilterChange: (string) => void; /** * Sets the state and sends a query on filter change. @@ -201,7 +194,7 @@ class MultiSelectAutocomplete extends Component { * @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 { } } - _onRetry: () => void; - /** * Retries the query on retry. * @@ -227,18 +218,16 @@ class MultiSelectAutocomplete extends Component { 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 { if (!this.state.error) { return null; } - const content = ( -
- -
- ); return ( - + + ); } - _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 { 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 { 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({ diff --git a/react/features/base/ui/components/types.ts b/react/features/base/ui/components/types.ts index e5fd8dbc7d..f3b72bdec5 100644 --- a/react/features/base/ui/components/types.ts +++ b/react/features/base/ui/components/types.ts @@ -105,3 +105,11 @@ export interface ISwitchProps { */ onChange: (on?: boolean) => void; } + +export type MultiSelectItem = { + content: string; + description?: string; + elemBefore?: Element; + isDisabled?: boolean; + value: string; +}; diff --git a/react/features/base/ui/components/web/Input.tsx b/react/features/base/ui/components/web/Input.tsx index cdb48a6a54..60e791de0f 100644 --- a/react/features/base/ui/components/web/Input.tsx +++ b/react/features/base/ui/components/web/Input.tsx @@ -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(({ maxRows, minRows, name, + onBlur, onChange, + onFocus, onKeyPress, placeholder, readOnly = false, @@ -208,7 +212,9 @@ const Input = React.forwardRef(({ { ...(id ? { id } : {}) } maxLength = { maxLength } name = { name } + onBlur = { onBlur } onChange = { handleChange } + onFocus = { onFocus } onKeyPress = { onKeyPress } placeholder = { placeholder } readOnly = { readOnly } diff --git a/react/features/base/ui/components/web/MultiSelect.tsx b/react/features/base/ui/components/web/MultiSelect.tsx new file mode 100644 index 0000000000..9bfc086dee --- /dev/null +++ b/react/features/base/ui/components/web/MultiSelect.tsx @@ -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(() => ( +
+ { + items.length > 0 + ? items.map(item => ( +
+ {item.elemBefore} +
+ {item.content} + {item.description &&

{item.description}

} +
+
+ )) + :
{noMatchesText}
+ } +
+ ), [ items ]); + + const errorMessageDialog = useMemo(() => + error &&
+ { errorDialog } +
, [ error ]); + + return ( +
+ + {isOpen && foundItems} + { errorMessageDialog } + { selectedItems && selectedItems?.length > 0 && ( +
+ { selectedItems.map(item => ( +
+ {item.elemBefore} +
+

{item.content}

+
+ +
+ ))} +
+ )} +
+ ); +}; + +export default MultiSelect; diff --git a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.ts b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx similarity index 87% rename from react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.ts rename to react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx index e9cd17ac14..ee5bdaecb2 100644 --- a/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.ts +++ b/react/features/invite/components/add-people-dialog/AbstractAddPeopleDialog.tsx @@ -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; - /** + /** * 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; + inviteItems: Array; } /** * Implements an abstract dialog to invite people to the conference. */ -export default class AbstractAddPeopleDialog

- extends Component { +export default class AbstractAddPeopleDialog

extends Component { /** * Constructor of the component. * @@ -112,7 +113,7 @@ export default class AbstractAddPeopleDialog

/** * 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

return invitee.address; } - return invitee.name; + return invitee.name ?? ''; } /** @@ -135,8 +136,8 @@ export default class AbstractAddPeopleDialog

* no invites left to send. If any are left, that means an invite failed * and an error state should display. * - * @param {Array} invitees - The items to be invited. - * @returns {Promise>} + * @param {Array} invitees - The items to be invited. + * @returns {Promise>} */ _invite(invitees: IInvitee[]) { const inviteTypeCounts = getInviteTypeCounts(invitees); @@ -176,10 +177,9 @@ export default class AbstractAddPeopleDialog

'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

notificationProps = { titleArguments: { name: this._getDisplayName(invitees[0]), - count: invitedCount - 1 + count: `${invitedCount - 1}` }, titleKey: 'notify.invitedThreePlusMembers' }; @@ -250,7 +250,7 @@ export default class AbstractAddPeopleDialog

_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) }; } diff --git a/react/features/invite/components/add-people-dialog/web/AddPeopleDialog.tsx b/react/features/invite/components/add-people-dialog/web/AddPeopleDialog.tsx index 0cbf3da7b8..68110321dc 100644 --- a/react/features/invite/components/add-people-dialog/web/AddPeopleDialog.tsx +++ b/react/features/invite/components/add-people-dialog/web/AddPeopleDialog.tsx @@ -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'; diff --git a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx similarity index 72% rename from react/features/invite/components/add-people-dialog/web/InviteContactsForm.js rename to react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx index c9e145cfb0..4bb38bec1d 100644 --- a/react/features/invite/components/add-people-dialog/web/InviteContactsForm.js +++ b/react/features/invite/components/add-people-dialog/web/InviteContactsForm.tsx @@ -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, + dispatch: IStore['dispatch']; /** * Invoked to obtain translated strings. */ - t: Function, -}; + t: Function; +} /** * Form that enables inviting others to the call. */ -class InviteContactsForm extends AbstractAddPeopleDialog { - _multiselect = null; +class InviteContactsForm extends AbstractAddPeopleDialog { + _multiselect: MultiSelectAutocomplete | null = null; - _resourceClient: Object; + _resourceClient: { + makeQuery: (query: string) => Promise>; + 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 { * @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 { /** * 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 { 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 { return (

- { this._renderErrorMessage() } { ); } - _invite: Array => 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 { return item; } - _onSelectionChange: (Map<*, *>) => void; - /** * Handles a selection change. * - * @param {Array} selectedItems - The list of selected items. + * @param {Array} 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 { 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 { } } - _parseQueryResults: (?Array) => Array; - /** * 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 ( ); @@ -305,7 +320,7 @@ class InviteContactsForm extends AbstractAddPeopleDialog { * @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 { ]; } - _query: (string) => Promise>; - - _onClearItems: () => void; - /** * Clears the selected items from state and form. * @@ -410,16 +421,14 @@ class InviteContactsForm extends AbstractAddPeopleDialog { 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 { */ _renderFormActions() { const { inviteItems } = this.state; - const { t } = this.props; + const { t, classes } = this.props; if (!inviteItems.length) { return null; } return ( - - ); - } - - /** - * 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 ? ( - - - { supportString.padEnd(supportString.length + 1) } - - - - { t('inlineDialogFailure.support') } - - - . - - ) : null; - - return ( -
- - { supportLinkContent } - + role = 'button' />
); } @@ -515,23 +478,19 @@ class InviteContactsForm extends AbstractAddPeopleDialog { */ _renderTelephoneIcon() { return ( - - - + ); } - _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 { * 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))); diff --git a/react/features/invite/components/add-people-dialog/web/InviteContactsSection.js b/react/features/invite/components/add-people-dialog/web/InviteContactsSection.tsx similarity index 60% rename from react/features/invite/components/add-people-dialog/web/InviteContactsSection.js rename to react/features/invite/components/add-people-dialog/web/InviteContactsSection.tsx index 3b64faade9..7f9ce38e26 100644 --- a/react/features/invite/components/add-people-dialog/web/InviteContactsSection.js +++ b/react/features/invite/components/add-people-dialog/web/InviteContactsSection.tsx @@ -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$} */ -function InviteContactsSection({ t }: Props) { +function InviteContactsSection() { + const { t } = useTranslation(); + return ( <> {t('addPeople.addContacts')} @@ -29,4 +20,4 @@ function InviteContactsSection({ t }: Props) { ); } -export default translate(InviteContactsSection); +export default InviteContactsSection; diff --git a/react/features/invite/types.ts b/react/features/invite/types.ts index 2f592ce920..ac8439bb82 100644 --- a/react/features/invite/types.ts +++ b/react/features/invite/types.ts @@ -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; } diff --git a/react/features/toolbox/constants.ts b/react/features/toolbox/constants.ts index 8fff98bccb..4723942858 100644 --- a/react/features/toolbox/constants.ts +++ b/react/features/toolbox/constants.ts @@ -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;