bookbrainz/bookbrainz-site

View on GitHub
src/client/components/pages/parts/search-results.js

Summary

Maintainability
D
2 days
Test Coverage
/*
 * Copyright (C) 2015  Ohm Patel
 *               2016  Sean Burke
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

import * as bootstrap from 'react-bootstrap';

import {differenceBy as _differenceBy, kebabCase as _kebabCase, startCase as _startCase, toLower} from 'lodash';

import AddToCollectionModal from './add-to-collection-modal';
import PropTypes from 'prop-types';
import React from 'react';
import {genEntityIconHTMLElement} from '../../../helpers/entity';


const {Alert, Badge, Button, ButtonGroup, Table} = bootstrap;

// Main entities have a BBID but some other indexed types
// have an ID field instead (collections, editors, areas)
function getId(entity) {
    return entity.bbid ?? entity.id;
}

/**
 * Renders the document and displays the 'SearchResults' page.
 * @returns {ReactElement} a HTML document which displays the SearchResults.
 * @param {object} props - Properties passed to the component.
 */
class SearchResults extends React.Component {
    constructor(props) {
        super(props);

        this.state = {
            message: {
                text: null,
                type: null
            },
            selected: [],
            showModal: false
        };

        this.handleAddToCollection = this.handleAddToCollection.bind(this);
        this.toggleRow = this.toggleRow.bind(this);
        this.onCloseModal = this.onCloseModal.bind(this);
        this.handleShowModal = this.handleShowModal.bind(this);
        this.handleAlertDismiss = this.handleAlertDismiss.bind(this);
        this.closeModalAndShowMessage = this.closeModalAndShowMessage.bind(this);
        this.handleClearSelected = this.handleClearSelected.bind(this);
    }

    onCloseModal() {
        this.setState({showModal: false});
    }

    handleShowModal() {
        if (this.props.user) {
            this.setState({showModal: true});
        }
        else {
            this.setState({
                message: {
                    text: 'You need to be logged in',
                    type: 'danger'
                }
            });
        }
    }

    closeModalAndShowMessage(message) {
        this.setState({
            message,
            showModal: false
        });
    }

    handleAlertDismiss() {
        this.setState({message: {}});
    }

    toggleRow(entity) {
        // eslint-disable-next-line react/no-access-state-in-setstate
        const oldSelected = this.state.selected;
        let newSelected;
        if (oldSelected.find(selected => getId(selected) === getId(entity))) {
            newSelected = oldSelected.filter(selected => getId(selected) !== getId(entity));
        }
        else {
            newSelected = [...oldSelected, entity];
        }
        this.setState({
            selected: newSelected
        });
    }

    handleClearSelected() {
        this.setState({selected: []});
    }

    handleAddToCollection() {
        const selectedEntities = this.state.selected;
        if (selectedEntities.length) {
            const areAllEntitiesOfSameType = selectedEntities.every(entity => entity.type === selectedEntities[0].type);
            const entityTypes = ['Author', 'Edition', 'EditionGroup', 'Publisher', 'Series', 'Work'];
            if (areAllEntitiesOfSameType) {
                if (entityTypes.includes(selectedEntities[0].type)) {
                    this.setState({message: {}, showModal: true});
                }
                else {
                    this.setState({
                        message: {
                            text: `${selectedEntities[0].type} cannot be added to a collection`,
                            type: 'danger'
                        }
                    });
                }
            }
            else {
                this.setState({
                    message: {
                        text: 'Selected entities should be of same type',
                        type: 'danger'
                    }
                });
            }
        }
        else {
            this.setState({
                message: {
                    text: 'Nothing Selected',
                    type: 'danger'
                }
            });
        }
    }

    render() {
        const noResults = !this.props.results || this.props.results.length === 0;

        const results = this.props.results.map((result) => {
            if (!result) {
                return null;
            }
            const id = getId(result);
            const name = result.defaultAlias ? result.defaultAlias.name :
                '(unnamed)';

            const aliases = !this.props.condensed && result.aliasSet &&
                Array.isArray(result.aliasSet.aliases) && result.aliasSet.aliases;

            const secondaryAliases = !this.props.condensed && aliases &&
                _differenceBy(aliases, [result.defaultAlias], 'id').map(alias => alias.name).join(', ');

            const disambiguation = result.disambiguation ? <small>({result.disambiguation.comment})</small> : '';
            // No redirect link for Area entity results
            const link = result.type === 'Area' ?
                `//musicbrainz.org/area/${id}` :
                `/${_kebabCase(result.type)}/${id}`;

            /* eslint-disable react/jsx-no-bind */
            return (
                <tr key={id}>
                    {
                        !this.props.condensed &&
                        <td>
                            {
                                this.props.user ?
                                    <input
                                        checked={this.state.selected.find(selected => getId(selected) === id)}
                                        className="checkboxes"
                                        type="checkbox"
                                        onChange={() => this.toggleRow(result)}
                                    /> : null
                            }
                            {genEntityIconHTMLElement(result.type)}{_startCase(result.type)}
                        </td>
                    }
                    <td>
                        <a href={link}>
                            {name} {disambiguation}
                        </a>
                        {
                            toLower(result.type) === 'work' && Boolean(result.authors?.length) &&
                            <span className="small text-muted"> — <i>{result.authors.join(', ')}</i></span>
                        }
                    </td>
                    {
                        !this.props.condensed &&
                        <td>
                            {secondaryAliases}
                        </td>
                    }
                </tr>
            );
        });
        let tableCssClasses = 'table table-striped';
        if (this.props.condensed) {
            tableCssClasses += ' table-condensed';
        }
        if (noResults) {
            return null;
        }
        return (
            <div>
                {
                    this.props.user ?
                        <div>
                            <AddToCollectionModal
                                bbids={this.state.selected.map(getId)}
                                closeModalAndShowMessage={this.closeModalAndShowMessage}
                                entityType={this.state.selected[0]?.type}
                                handleCloseModal={this.onCloseModal}
                                show={this.state.showModal}
                                userId={this.props.user.id}
                            />
                        </div> : null
                }
                {
                    !this.props.condensed &&
                    <h3 className="search-results-heading">
                        Search Results
                    </h3>
                }
                <hr className="thin"/>
                <Table
                    responsive
                    className={tableCssClasses}
                >
                    {
                        !this.props.condensed &&
                        <thead>
                            <tr>
                                <th width="25%">Type</th>
                                <th width="42%">Name</th>
                                <th width="33%">Aliases</th>
                            </tr>
                        </thead>
                    }
                    <tbody>
                        {results}
                    </tbody>
                </Table>
                {
                    this.state.message.text ?
                        <Alert
                            className="margin-top-1"
                            variant={this.state.message.type}
                            onDismiss={this.handleAlertDismiss}
                        >
                            {this.state.message.text}
                        </Alert> : null

                }
                {
                    this.props.user ?
                        <ButtonGroup>
                            <Button
                                disabled={!this.state.selected.length}
                                type="button"
                                variant="primary"
                                onClick={this.handleAddToCollection}
                            >
                                {genEntityIconHTMLElement('Collection')}
                                    Add to Collection
                            </Button>
                            <Button
                                disabled={!this.state.selected.length}
                                type="button"
                                variant="warning"
                                onClick={this.handleClearSelected}
                            >
                                Clear <Badge pill variant="light">{this.state.selected.length}</Badge> selected
                            </Button>
                        </ButtonGroup> : null
                }
            </div>
        );
    }
}

SearchResults.displayName = 'SearchResults';
SearchResults.propTypes = {
    condensed: PropTypes.bool,
    results: PropTypes.array,
    user: PropTypes.object.isRequired
};
SearchResults.defaultProps = {
    condensed: false,
    results: null
};

export default SearchResults;