import React from 'react'; import {OrderedMap, Set} from 'immutable'; import { IWebsocketMessage, IResourceUpdateEvent, IResourceDeletedEvent, } from 'superdesk-api'; import {addWebsocketEventListener} from './notification/notification'; import {throttleAndCombineSet} from './itemList/throttleAndCombine'; export interface IMultiSelectOptions { selected: OrderedMap; select(item: T): void; selectMultiple(items: OrderedMap): void; unselect(item: T): void; unselectAll(): void; toggle(item: T): void; } interface IProps { getId(item: T): string; // used to listen for websocket events in order to decide if items have to be unselected resourceNames: Array; children: (options: IMultiSelectOptions) => JSX.Element; /** * When items are updated/deleted, we need to check if they should be unselected * in case they no longer match the query and thus are no longer visible * in the list view. */ shouldUnselect(ids: Set): Promise>; } interface IState { selected: OrderedMap; } export class MultiSelectHoc extends React.PureComponent, IState> { private removeContentUpdateListener: () => void; private removeResourceDeletedListener: () => void; private maybeUnselectItems: (ids: globalThis.Set) => void; constructor(props: IProps) { super(props); this.state = { selected: OrderedMap(), }; this.select = this.select.bind(this); this.selectMultiple = this.selectMultiple.bind(this); this.unselect = this.unselect.bind(this); this.toggle = this.toggle.bind(this); this.unselectAll = this.unselectAll.bind(this); this.handleContentChanges = this.handleContentChanges.bind(this); this.maybeUnselectItems = throttleAndCombineSet(this._maybeUnselectItems.bind(this), 500); } select(item: T) { const {getId} = this.props; this.setState({selected: this.state.selected.set(getId(item), item)}); } selectMultiple(items: OrderedMap): void { this.setState({selected: this.state.selected.merge(items)}); } unselect(item: T) { const {getId} = this.props; this.setState({selected: this.state.selected.remove(getId(item))}); } toggle(item: T) { const {getId} = this.props; if (this.state.selected.has(getId(item))) { this.unselect(item); } else { this.select(item); } } unselectAll() { this.setState({selected: OrderedMap()}); } _maybeUnselectItems(ids: globalThis.Set) { // only throttled version should be used internally this.props.shouldUnselect(Set(Array.from(ids))).then((idsToUnselect) => { if (idsToUnselect.size > 0) { let {selected} = this.state; idsToUnselect.forEach((_id) => { selected = selected.remove(_id); }); this.setState({selected}); } }); } handleContentChanges(resource: string, id: string) { // Unselect items that no longer match the query. if (this.props.resourceNames.includes(resource) && this.state.selected.has(id)) { this.maybeUnselectItems(new global.Set([id])); } } componentDidMount() { // Skipping created event, because a resource that is not created will not be selected. this.removeContentUpdateListener = addWebsocketEventListener( 'resource:updated', (event: IWebsocketMessage) => { const {resource, _id} = event.extra; this.handleContentChanges(resource, _id); }, ); this.removeResourceDeletedListener = addWebsocketEventListener( 'resource:deleted', (event: IWebsocketMessage) => { const {resource, _id} = event.extra; this.handleContentChanges(resource, _id); }, ); } componentWillUnmount() { this.removeContentUpdateListener(); this.removeResourceDeletedListener(); } render() { return this.props.children({ selected: this.state.selected, select: this.select, selectMultiple: this.selectMultiple, unselect: this.unselect, unselectAll: this.unselectAll, toggle: this.toggle, }); } }