/* * SonarQube * Copyright (C) 2009-2019 SonarSource SA * mailto:info AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser 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 React from 'react'; import * as classNames from 'classnames'; import { debounce } from 'lodash'; import { Component } from '@sqapi/components'; import ProjectSelectorItem from './ProjectSelectorItem'; interface Props { isLoggedIn: boolean; onQueryChange: (query: string) => Promise; onSelect: (component: Component) => void; projects: Component[]; selected?: Component; } interface State { activeIdx: number; activeKey?: string; favorite: boolean; open: boolean; search: string; searching: boolean; } export default class ProjectSelector extends React.PureComponent { node?: HTMLElement | null; debouncedHandleSearch: () => void; constructor(props: Props) { super(props); const firstProject = props.projects[0]; this.state = { activeIdx: firstProject ? 0 : -1, activeKey: firstProject && firstProject.key, favorite: props.isLoggedIn, open: false, search: '', searching: false }; this.debouncedHandleSearch = debounce(this.handleSearch, 250); } componentDidMount() { window.addEventListener('click', this.handleClickOutside); if (this.node) { this.node.addEventListener('keydown', this.handleKeyDown, true); } } componentWillReceiveProps(nextProps: Props) { if (this.props.projects !== nextProps.projects) { let activeIdx = nextProps.projects.findIndex(project => project.key === this.state.activeKey); activeIdx = activeIdx >= 0 ? activeIdx : 0; this.setState({ activeIdx, activeKey: this.getActiveKey(activeIdx) }); } } componentWillUnmount() { window.removeEventListener('click', this.handleClickOutside); if (this.node) { this.node.removeEventListener('keydown', this.handleKeyDown); } } getActiveKey = (idx: number) => { const { projects } = this.props; return projects[idx] && projects[idx].key; }; getEmptyMessage = () => { const { favorite, search } = this.state; if (search) { return 'No project matching your search.'; } else if (favorite) { return "You don't have any favorite projects yet."; } return 'No project have been found'; }; handleClickOutside = (event: Event) => { if (!this.node || !this.node.contains(event.target as HTMLElement)) { this.setState({ open: false }); } }; handleFilterAll = () => { this.setState({ favorite: false, searching: true }, this.handleSearch); }; handleFilterFavorite = () => { this.setState({ favorite: true, searching: true }, this.handleSearch); }; handleItemHover = (item: Component) => { let activeIdx = this.props.projects.findIndex(project => project.key === item.key); activeIdx = activeIdx >= 0 ? activeIdx : 0; this.setState({ activeIdx, activeKey: this.getActiveKey(activeIdx) }); }; handleKeyDown = (evt: KeyboardEvent) => { switch (evt.keyCode) { case 40: // down evt.stopPropagation(); evt.preventDefault(); this.setState(this.selectNextItem); break; case 38: // up evt.stopPropagation(); evt.preventDefault(); this.setState(this.selectPreviousItem); break; case 37: // left case 39: // right evt.stopPropagation(); break; case 13: // enter if (this.state.activeIdx >= 0) { this.handleSelect(this.props.projects[this.state.activeIdx]); } break; case 27: // escape this.setState({ open: false }); break; } }; handleSearchChange = (event: React.ChangeEvent) => { this.setState( { search: event.currentTarget.value, searching: true }, this.debouncedHandleSearch ); }; handleSearch = () => { const filter = []; if (this.state.favorite) { filter.push('isFavorite'); } if (this.state.search) { filter.push(`query = "${this.state.search}"`); } this.props.onQueryChange(filter.join(' and ')).then(this.stopSearching, this.stopSearching); }; handleSelect = (project: Component) => { this.props.onSelect(project); this.setState({ open: false }); }; selectNextItem = ({ activeIdx }: State, { projects }: Props) => { let newActiveIdx = activeIdx + 1; if (activeIdx < 0 || activeIdx >= projects.length - 1) { newActiveIdx = 0; } return { activeIdx: newActiveIdx, activeKey: this.getActiveKey(newActiveIdx) }; }; selectPreviousItem = ({ activeIdx }: State, { projects }: Props) => { let newActiveIdx = activeIdx - 1; if (activeIdx <= 0) { newActiveIdx = projects.length - 1; } return { activeIdx: newActiveIdx, activeKey: this.getActiveKey(newActiveIdx) }; }; stopSearching = () => { this.setState({ searching: false }); }; toggleOpen = () => { this.setState(({ open }) => ({ open: !open })); }; render() { const { isLoggedIn, projects, selected } = this.props; const { activeIdx, favorite, open, search, searching } = this.state; return (
(this.node = node)}>
{selected ? selected.name : 'Select a project...'}
{open && (
{isLoggedIn && ( )}
{searching && }
    {projects.map((project, idx) => ( ))} {projects.length <= 0 && (
  • {this.getEmptyMessage()}
  • )}
)}
); } }