Files
jitsi-meet/react/features/base/react/components/web/MultiSelectAutocomplete.js
Leonard Kim 486e8e35d9 ref: move all prop type declaration to flow
For the most part the changes are taking the "static propTypes" declaration off
of components and declaring them as Flow types. Sometimes to support flow some
method signatures had to be added. There are some exceptions in which more had
to be done to tame the beast:
- AbstractVideoTrack: put in additional truthy checks for videoTrack.
- Video: add truthy checks for the _videoElement ref.
- shouldRenderVideoTrack function: Some component could pass null for the
  videoTrack argument and Flow wanted that called out explicitly.
- DisplayName: Add a truthy check for the input ref before acting on it.
- NumbersList: Move array checks inline for Flow to comprehend array methods
  could be called. Add type checks in the Object.entries loop as the value is
  assumed to be a mixed type by Flow.
- AbstractToolbarButton: add additional truthy check for passed in type.
2018-11-07 17:38:10 +01:00

334 lines
8.5 KiB
JavaScript

// @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;