// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' import {BlockIcons} from '../blockIcons' import {IPropertyTemplate} from '../blocks/board' import {ISortOption, MutableBoardView} from '../blocks/boardView' import {MutableCard} from '../blocks/card' import {CardFilter} from '../cardFilter' import ViewMenu from '../components/viewMenu' import {Constants} from '../constants' import {CsvExporter} from '../csvExporter' import mutator from '../mutator' import {UserContext} from '../user' import {BoardTree} from '../viewModel/boardTree' import Button from '../widgets/buttons/button' import IconButton from '../widgets/buttons/iconButton' import CheckIcon from '../widgets/icons/check' import DropdownIcon from '../widgets/icons/dropdown' import OptionsIcon from '../widgets/icons/options' import SortDownIcon from '../widgets/icons/sortDown' import SortUpIcon from '../widgets/icons/sortUp' import Menu from '../widgets/menu' import MenuWrapper from '../widgets/menuWrapper' import Editable from './editable' import FilterComponent from './filterComponent' import ModalWrapper from './modalWrapper' import NewCardButton from './newCardButton' import ShareBoardComponent from './shareBoardComponent' import './viewHeader.scss' import {sendFlashMessage} from './flashMessages' type Props = { boardTree: BoardTree showView: (id: string) => void setSearchText: (text?: string) => void addCard: () => void addCardFromTemplate: (cardTemplateId: string) => void addCardTemplate: () => void editCardTemplate: (cardTemplateId: string) => void withGroupBy?: boolean intl: IntlShape readonly: boolean } type State = { isSearching: boolean showFilter: boolean showShareDialog: boolean } class ViewHeader extends React.Component { private searchFieldRef = React.createRef() shouldComponentUpdate(): boolean { return true } constructor(props: Props) { super(props) this.state = {isSearching: Boolean(this.props.boardTree.getSearchText()), showFilter: false, showShareDialog: false} } componentDidUpdate(prevPros: Props, prevState: State): void { if (this.state.isSearching && !prevState.isSearching) { this.searchFieldRef.current?.focus() } } onExportCsvTrigger(boardTree: BoardTree) { try { CsvExporter.exportTableCsv(boardTree) const exportCompleteMessage = this.props.intl.formatMessage({ id: 'ViewHeader.export-complete', defaultMessage: 'Export complete!', }) sendFlashMessage({content: exportCompleteMessage, severity: 'normal'}) } catch (e) { const exportFailedMessage = this.props.intl.formatMessage({ id: 'ViewHeader.export-failed', defaultMessage: 'Export failed!', }) sendFlashMessage({content: exportFailedMessage, severity: 'high'}) } } render(): JSX.Element { const {boardTree, showView, withGroupBy, intl} = this.props const {board, activeView} = boardTree const hasFilter = activeView.filter && activeView.filter.filters?.length > 0 const hasSort = activeView.sortOptions.length > 0 return (
{ mutator.changeTitle(activeView, text) }} readonly={this.props.readonly} /> }/>
{!this.props.readonly && <> {/* Card properties */} {boardTree.board.cardProperties.map((option: IPropertyTemplate) => ( { let newVisiblePropertyIds = [] if (activeView.visiblePropertyIds.includes(propertyId)) { newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o: string) => o !== propertyId) } else { newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId] } mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) }} /> ))} {/* Group by */} {withGroupBy && {boardTree.board.cardProperties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => ( : undefined} onClick={(id) => { if (boardTree.activeView.groupById === id) { return } mutator.changeViewGroupById(boardTree.activeView, id) }} /> ))} } {/* Filter */} {this.state.showFilter && } {/* Sort */} {(activeView.sortOptions.length > 0) && <> { // This sets the manual card order to the currently displayed order // Note: Perform this as a single update to change both properties correctly const newView = new MutableBoardView(activeView) newView.cardOrder = boardTree.orderedCards().map((o) => o.id) newView.sortOptions = [] mutator.updateBlock(newView, activeView, 'reorder') }} /> { mutator.changeViewSortOptions(activeView, []) }} /> } {this.sortDisplayOptions().map((option) => { let rightIcon: JSX.Element | undefined if (activeView.sortOptions.length > 0) { const sortOption = activeView.sortOptions[0] if (sortOption.propertyId === option.id) { rightIcon = sortOption.reversed ? : } } return ( { let newSortOptions: ISortOption[] = [] if (activeView.sortOptions[0] && activeView.sortOptions[0].propertyId === propertyId) { // Already sorting by name, so reverse it newSortOptions = [ {propertyId, reversed: !activeView.sortOptions[0].reversed}, ] } else { newSortOptions = [ {propertyId, reversed: false}, ] } mutator.changeViewSortOptions(activeView, newSortOptions) }} /> ) })} } {/* Search */} {this.state.isSearching && { this.searchChanged(text) }} onKeyDown={(e) => { this.onSearchKeyDown(e) }} /> } {!this.state.isSearching && } {/* Options menu */} {!this.props.readonly && <> }/> this.onExportCsvTrigger(boardTree)} /> {/* Archiver.exportBoardTree(boardTree)} /> */} {(user) => (user && user.id !== 'single-user' && )} {/* this.testAddCards(100)} /> this.testAddCards(1000)} /> this.testDistributeCards()} /> this.testRandomizeIcons()} /> */} {this.state.showShareDialog && } {/* New card button */} }
) } private showFilterDialog = () => { this.setState({showFilter: true}) } private hideFilterDialog = () => { this.setState({showFilter: false}) } private showShareDialog = () => { this.setState({showShareDialog: true}) } private hideShareDialog = () => { this.setState({showShareDialog: false}) } private onSearchKeyDown = (e: React.KeyboardEvent) => { if (e.keyCode === 27) { // ESC: Clear search if (this.searchFieldRef.current) { this.searchFieldRef.current.text = '' } this.setState({isSearching: false}) this.props.setSearchText(undefined) e.preventDefault() } } private searchChanged(text?: string) { this.props.setSearchText(text) } private async testAddCards(count: number) { const {boardTree} = this.props const {board, activeView} = boardTree const startCount = boardTree.cards.length let optionIndex = 0 mutator.performAsUndoGroup(async () => { for (let i = 0; i < count; i++) { const card = new MutableCard() card.parentId = boardTree.board.id card.rootId = boardTree.board.rootId card.properties = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties) card.title = `Test Card ${startCount + i + 1}` card.icon = BlockIcons.shared.randomIcon() if (boardTree.groupByProperty && boardTree.groupByProperty.options.length > 0) { // Cycle through options const option = boardTree.groupByProperty.options[optionIndex] optionIndex = (optionIndex + 1) % boardTree.groupByProperty.options.length card.properties[boardTree.groupByProperty.id] = option.id } mutator.insertBlock(card, 'test add card') } }) } private async testDistributeCards() { const {boardTree} = this.props mutator.performAsUndoGroup(async () => { let optionIndex = 0 for (const card of boardTree.cards) { if (boardTree.groupByProperty && boardTree.groupByProperty.options.length > 0) { // Cycle through options const option = boardTree.groupByProperty.options[optionIndex] optionIndex = (optionIndex + 1) % boardTree.groupByProperty.options.length const newCard = new MutableCard(card) if (newCard.properties[boardTree.groupByProperty.id] !== option.id) { newCard.properties[boardTree.groupByProperty.id] = option.id mutator.updateBlock(newCard, card, 'test distribute cards') } } } }) } private async testRandomizeIcons() { const {boardTree} = this.props mutator.performAsUndoGroup(async () => { for (const card of boardTree.cards) { mutator.changeIcon(card, BlockIcons.shared.randomIcon(), 'randomize icon') } }) } private sortDisplayOptions() { const {boardTree} = this.props const options = boardTree.board.cardProperties.map((o) => ({id: o.id, name: o.name})) options.unshift({id: Constants.titleColumnId, name: 'Name'}) return options } } export default injectIntl(ViewHeader)