superdesk/superdesk-client-core

View on GitHub
scripts/core/MultiSelectHoc.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
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<T> {
    selected: OrderedMap<string, T>;
    select(item: T): void;
    selectMultiple(items: OrderedMap<string, T>): void;
    unselect(item: T): void;
    unselectAll(): void;
    toggle(item: T): void;
}

interface IProps<T> {
    getId(item: T): string;

    // used to listen for websocket events in order to decide if items have to be unselected
    resourceNames: Array<string>;

    children: (options: IMultiSelectOptions<T>) => 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<string>): Promise<Set<string>>;
}

interface IState<T> {
    selected: OrderedMap<string, T>;
}

export class MultiSelectHoc<T> extends React.PureComponent<IProps<T>, IState<T>> {
    private removeContentUpdateListener: () => void;
    private removeResourceDeletedListener: () => void;
    private maybeUnselectItems: (ids: globalThis.Set<string>) => void;

    constructor(props: IProps<T>) {
        super(props);

        this.state = {
            selected: OrderedMap<string, T>(),
        };

        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<string, T>): 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<string, T>()});
    }
    _maybeUnselectItems(ids: globalThis.Set<string>) { // 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<IResourceUpdateEvent>) => {
                const {resource, _id} = event.extra;

                this.handleContentChanges(resource, _id);
            },
        );

        this.removeResourceDeletedListener = addWebsocketEventListener(
            'resource:deleted',
            (event: IWebsocketMessage<IResourceDeletedEvent>) => {
                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,
        });
    }
}