// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react' import {FormattedMessage} from 'react-intl' import {Archiver} from '../archiver' import {ISortOption} from '../blocks/boardView' import {BlockIcons} from '../blockIcons' import {IPropertyTemplate} from '../blocks/board' import {Card, MutableCard} from '../blocks/card' import {BoardTree} from '../viewModel/boardTree' import ViewMenu from '../components/viewMenu' import {CsvExporter} from '../csvExporter' import {Menu as OldMenu} from '../menu' import mutator from '../mutator' import {Utils} from '../utils' import Menu from '../widgets/menu' import MenuWrapper from '../widgets/menuWrapper' import Button from './button' import {CardDialog} from './cardDialog' import {Editable} from './editable' import RootPortal from './rootPortal' import {TableRow} from './tableRow' type Props = { boardTree?: BoardTree showView: (id: string) => void showFilter: (el: HTMLElement) => void setSearchText: (text: string) => void } type State = { isHoverOnCover: boolean isSearching: boolean shownCard?: Card viewMenu: boolean } class TableComponent extends React.Component { private draggedHeaderTemplate: IPropertyTemplate private cardIdToRowMap = new Map>() private cardIdToFocusOnRender: string private searchFieldRef = React.createRef() constructor(props: Props) { super(props) this.state = {isHoverOnCover: false, isSearching: Boolean(this.props.boardTree?.getSearchText()), viewMenu: false} } shouldComponentUpdate(): boolean { return true } componentDidUpdate(prevPros: Props, prevState: State): void { if (this.state.isSearching && !prevState.isSearching) { this.searchFieldRef.current.focus() } } render(): JSX.Element { const {boardTree, showView} = this.props if (!boardTree || !boardTree.board) { return (
) } const {board, cards, activeView} = boardTree const hasFilter = activeView.filter && activeView.filter.filters?.length > 0 const hasSort = activeView.sortOptions.length > 0 this.cardIdToRowMap.clear() return (
{this.state.shownCard && this.setState({shownCard: undefined})} /> }
{ this.setState({...this.state, isHoverOnCover: true}) }} onMouseLeave={() => { this.setState({...this.state, isHoverOnCover: false}) }} >
{board.icon &&
{board.icon}
{(text: string) => ( mutator.changeIcon(board, BlockIcons.shared.randomIcon())} /> )} {(text: string) => ( mutator.changeIcon(board, undefined, 'remove icon')} /> )}
} {(placeholder: string) => ( { mutator.changeTitle(board, text) }} /> )}
{ mutator.changeTitle(activeView, text) }} />
{boardTree.board.cardProperties.map((option) => ( { const property = boardTree.board.cardProperties.find((o) => o.id === propertyId) Utils.assertValue(property) Utils.log(`Toggle property ${property.name}`) let newVisiblePropertyIds = [] if (activeView.visiblePropertyIds.includes(propertyId)) { newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== propertyId) } else { newVisiblePropertyIds = [...activeView.visiblePropertyIds, propertyId] } mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) }} /> ))}
{ this.filterClicked(e) }} >
{boardTree.board.cardProperties.map((option) => ( { 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) }} /> ))}
{this.state.isSearching && {(placeholder: string) => ( { this.searchChanged(text) }} onKeyDown={(e) => { this.onSearchKeyDown(e) }} /> )} } {!this.state.isSearching &&
{ this.setState({...this.state, isSearching: true}) }} >
}
this.optionsClicked(e)} >
{ this.addCard(true) }} >
{/* Main content */}
{/* Headers */}
{ this.headerClicked(e, '__name') }} >
{board.cardProperties. filter((template) => activeView.visiblePropertyIds.includes(template.id)). map((template) => (
{ this.draggedHeaderTemplate = template }} onDragEnd={() => { this.draggedHeaderTemplate = undefined }} onDragOver={(e) => { e.preventDefault(); (e.target as HTMLElement).classList.add('dragover') }} onDragEnter={(e) => { e.preventDefault(); (e.target as HTMLElement).classList.add('dragover') }} onDragLeave={(e) => { e.preventDefault(); (e.target as HTMLElement).classList.remove('dragover') }} onDrop={(e) => { e.preventDefault(); (e.target as HTMLElement).classList.remove('dragover'); this.onDropToColumn(template) }} >
{ this.headerClicked(e, template.id) }} >{template.name}
), )}
{/* Rows, one per card */} {cards.map((card) => { const openButonRef = React.createRef() const tableRowRef = React.createRef() let focusOnMount = false if (this.cardIdToFocusOnRender && this.cardIdToFocusOnRender === card.id) { this.cardIdToFocusOnRender = undefined focusOnMount = true } const tableRow = ( { if (e.keyCode === 13) { // Enter: Insert new card if on last row if (cards.length > 0 && cards[cards.length - 1] === card) { this.addCard(false) } } }} />) this.cardIdToRowMap.set(card.id, tableRowRef) return tableRow })} {/* Add New row */}
{ this.addCard() }} >
) } private filterClicked(e: React.MouseEvent) { this.props.showFilter(e.target as HTMLElement) } private async optionsClicked(e: React.MouseEvent) { const {boardTree} = this.props OldMenu.shared.options = [ {id: 'exportCsv', name: 'Export to CSV'}, {id: 'exportBoardArchive', name: 'Export board archive'}, ] OldMenu.shared.onMenuClicked = async (id: string) => { switch (id) { case 'exportCsv': { CsvExporter.exportTableCsv(boardTree) break } case 'exportBoardArchive': { Archiver.exportBoardTree(boardTree) break } } } OldMenu.shared.showAtElement(e.target as HTMLElement) } private async headerClicked(e: React.MouseEvent, templateId: string) { const {boardTree} = this.props const {board} = boardTree const {activeView} = boardTree const options = [ {id: 'sortAscending', name: 'Sort ascending'}, {id: 'sortDescending', name: 'Sort descending'}, {id: 'insertLeft', name: 'Insert left'}, {id: 'insertRight', name: 'Insert right'}, ] if (templateId !== '__name') { options.push({id: 'hide', name: 'Hide'}) options.push({id: 'duplicate', name: 'Duplicate'}) options.push({id: 'delete', name: 'Delete'}) } OldMenu.shared.options = options OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => { switch (optionId) { case 'sortAscending': { const newSortOptions = [ {propertyId: templateId, reversed: false}, ] await mutator.changeViewSortOptions(activeView, newSortOptions) break } case 'sortDescending': { const newSortOptions = [ {propertyId: templateId, reversed: true}, ] await mutator.changeViewSortOptions(activeView, newSortOptions) break } case 'insertLeft': { if (templateId !== '__name') { const index = board.cardProperties.findIndex((o) => o.id === templateId) await mutator.insertPropertyTemplate(boardTree, index) } else { // TODO: Handle name column } break } case 'insertRight': { if (templateId !== '__name') { const index = board.cardProperties.findIndex((o) => o.id === templateId) + 1 await mutator.insertPropertyTemplate(boardTree, index) } else { // TODO: Handle name column } break } case 'duplicate': { await mutator.duplicatePropertyTemplate(boardTree, templateId) break } case 'hide': { const newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== templateId) await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) break } case 'delete': { await mutator.deleteProperty(boardTree, templateId) break } default: { Utils.assertFailure(`Unexpected menu option: ${optionId}`) break } } } OldMenu.shared.showAtElement(e.target as HTMLElement) } private focusOnCardTitle(cardId: string): void { const tableRowRef = this.cardIdToRowMap.get(cardId) Utils.log(`focusOnCardTitle, ${tableRowRef?.current ?? 'undefined'}`) tableRowRef?.current.focusOnTitle() } private async addCard(show = false) { const {boardTree} = this.props const card = new MutableCard() card.parentId = boardTree.board.id card.icon = BlockIcons.shared.randomIcon() await mutator.insertBlock( card, 'add card', async () => { if (show) { this.setState({shownCard: card}) } else { // Focus on this card's title inline on next render this.cardIdToFocusOnRender = card.id } }, ) } private async onDropToColumn(template: IPropertyTemplate) { const {draggedHeaderTemplate} = this if (!draggedHeaderTemplate) { return } const {boardTree} = this.props const {board} = boardTree Utils.assertValue(mutator) Utils.assertValue(boardTree) Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`) // Move template to new index const destIndex = template ? board.cardProperties.indexOf(template) : 0 await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex) } private onSearchKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 27) { // ESC: Clear search this.searchFieldRef.current.text = '' this.setState({...this.state, isSearching: false}) this.props.setSearchText(undefined) e.preventDefault() } } private searchChanged(text?: string) { this.props.setSearchText(text) } } export {TableComponent}