| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333 | // @flow
import { MultiSelectStateless } from '@atlaskit/multi-select';
import AKInlineDialog from '@atlaskit/inline-dialog';
import _debounce from 'lodash/debounce';
import React, { Component } from 'react';
import InlineDialogFailure from './InlineDialogFailure';
const logger = require('jitsi-meet-logger').getLogger(__filename);
/**
 * The type of the React {@code Component} props of
 * {@link MultiSelectAutocomplete}.
 */
type Props = {
    /**
     * The default value of the selected item.
     */
    defaultValue: Array<Object>,
    /**
     * Optional footer to show as a last element in the results.
     * Should be of type {content: <some content>}
     */
    footer: Object,
    /**
     * Indicates if the component is disabled.
     */
    isDisabled: boolean,
    /**
     * Text to display while a query is executing.
     */
    loadingMessage: string,
    /**
     * The text to show when no matches are found.
     */
    noMatchesFound: string,
    /**
     * The function called immediately before a selection has been actually
     * selected. Provides an opportunity to do any formatting.
     */
    onItemSelected: Function,
    /**
     * The function called when the selection changes.
     */
    onSelectionChange: Function,
    /**
     * The placeholder text of the input component.
     */
    placeholder: string,
    /**
     * The service providing the search.
     */
    resourceClient: { makeQuery: Function, parseResults: Function },
    /**
     * Indicates if the component should fit the container.
     */
    shouldFitContainer: boolean,
    /**
     * Indicates if we should focus.
     */
    shouldFocus: 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,
    /**
     * Indicates if there was an error.
     */
    error: boolean,
    /**
     * The list of result items.
     */
    items: Array<Object>,
    /**
     * The list of selected items.
     */
    selectedItems: Array<Object>
};
/**
 * A MultiSelect that is also auto-completing.
 */
class MultiSelectAutocomplete extends Component<Props, State> {
    /**
     * 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) {
        super(props);
        const defaultValue = this.props.defaultValue || [];
        this.state = {
            isOpen: false,
            filterValue: '',
            loading: false,
            error: false,
            items: [],
            selectedItems: [ ...defaultValue ]
        };
        this._onFilterChange = this._onFilterChange.bind(this);
        this._onRetry = this._onRetry.bind(this);
        this._onSelectionChange = this._onSelectionChange.bind(this);
        this._sendQuery = _debounce(this._sendQuery.bind(this), 200);
    }
    /**
     * Sets the items to display as selected.
     *
     * @param {Array<Object>} selectedItems - The list of items to display as
     * having been selected.
     * @returns {void}
     */
    setSelectedItems(selectedItems: Array<Object> = []) {
        this.setState({ selectedItems });
    }
    /**
     * Renders the content of this component.
     *
     * @returns {ReactElement}
     */
    render() {
        const shouldFitContainer = this.props.shouldFitContainer || false;
        const shouldFocus = this.props.shouldFocus || false;
        const isDisabled = this.props.isDisabled || false;
        const placeholder = this.props.placeholder || '';
        const noMatchesFound = this.props.noMatchesFound || '';
        return (
            <div>
                <MultiSelectStateless
                    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 }
                    onFilterChange = { this._onFilterChange }
                    onRemoved = { this._onSelectionChange }
                    onSelected = { this._onSelectionChange }
                    placeholder = { placeholder }
                    selectedItems = { this.state.selectedItems }
                    shouldFitContainer = { shouldFitContainer }
                    shouldFocus = { shouldFocus } />
                { this._renderError() }
            </div>
        );
    }
    _onFilterChange: (string) => void;
    /**
     * Sets the state and sends a query on filter change.
     *
     * @param {string} filterValue - The filter text value.
     * @private
     * @returns {void}
     */
    _onFilterChange(filterValue) {
        this.setState({
            // Clean the error if the filterValue is empty.
            error: this.state.error && Boolean(filterValue),
            filterValue,
            isOpen: Boolean(this.state.items.length) && Boolean(filterValue),
            items: filterValue ? this.state.items : [],
            loading: Boolean(filterValue)
        });
        if (filterValue) {
            this._sendQuery(filterValue);
        }
    }
    _onRetry: () => void;
    /**
     * Retries the query on retry.
     *
     * @private
     * @returns {void}
     */
    _onRetry() {
        this._sendQuery(this.state.filterValue);
    }
    _onSelectionChange: (Object) => void;
    /**
     * Updates the selected items when a selection event occurs.
     *
     * @param {Object} item - The selected item.
     * @private
     * @returns {void}
     */
    _onSelectionChange(item) {
        const existing
            = this.state.selectedItems.find(k => k.value === item.value);
        let selectedItems = this.state.selectedItems;
        if (existing) {
            selectedItems = selectedItems.filter(k => k !== existing);
        } else {
            selectedItems.push(this.props.onItemSelected(item));
        }
        this.setState({
            isOpen: false,
            selectedItems
        });
        if (this.props.onSelectionChange) {
            this.props.onSelectionChange(selectedItems);
        }
    }
    /**
     * Renders the error UI.
     *
     * @returns {ReactElement|null}
     */
    _renderError() {
        if (!this.state.error) {
            return null;
        }
        const content = (
            <div className = 'autocomplete-error'>
                <InlineDialogFailure
                    onRetry = { this._onRetry } />
            </div>
        );
        return (
            <AKInlineDialog
                content = { content }
                isOpen = { true } />
        );
    }
    _sendQuery: (string) => void;
    /**
     * Sends a query to the resourceClient.
     *
     * @param {string} filterValue - The string to use for the search.
     * @returns {void}
     */
    _sendQuery(filterValue) {
        if (!filterValue) {
            return;
        }
        this.setState({
            error: false
        });
        const resourceClient = this.props.resourceClient || {
            makeQuery: () => Promise.resolve([]),
            parseResults: results => results
        };
        resourceClient.makeQuery(filterValue)
            .then(results => {
                if (this.state.filterValue !== filterValue) {
                    this.setState({
                        error: false
                    });
                    return;
                }
                const itemGroups = [
                    {
                        items: resourceClient.parseResults(results)
                    }
                ];
                this.setState({
                    items: itemGroups,
                    isOpen: true,
                    loading: false,
                    error: false
                });
            })
            .catch(error => {
                logger.error('MultiSelectAutocomplete error in query', error);
                this.setState({
                    error: true,
                    loading: false,
                    isOpen: false
                });
            });
    }
}
export default MultiSelectAutocomplete;
 |