// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import React from 'react'; import {Archiver} from '../archiver'; import {BlockIcons} from '../blockIcons'; import {IPropertyTemplate} from '../blocks/board'; import {Card} from '../blocks/card'; import {BoardTree} from '../boardTree'; import ViewMenu from '../components/viewMenu'; import {CsvExporter} from '../csvExporter'; import {Menu as OldMenu} from '../menu'; import mutator from '../mutator'; import {OctoUtils} from '../octoUtils'; 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} } componentDidUpdate(prevPros: Props, prevState: State) { if (this.state.isSearching && !prevState.isSearching) { this.searchFieldRef.current.focus() } } render() { const {boardTree, showView} = this.props if (!boardTree || !boardTree.board) { return (
Loading...
) } 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}
mutator.changeIcon(board, BlockIcons.shared.randomIcon())} /> mutator.changeIcon(board, undefined, 'remove icon')} />
: undefined} { mutator.changeTitle(board, text) }} />
{ mutator.changeTitle(activeView, text) }} />
{ this.propertiesClicked(e) }} >Properties
{ this.filterClicked(e) }} >Filter
{ OctoUtils.showSortMenu(e, boardTree) }} >Sort
{this.state.isSearching ? { this.searchChanged(text) }} onKeyDown={(e) => { this.onSearchKeyDown(e) }} /> :
{ this.setState({...this.state, isSearching: true}) }} >Search
}
this.optionsClicked(e)} >
{ this.addCard(true) }} >New
{/* Main content */}
{/* Headers */}
{ this.headerClicked(e, '__name') }} >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() }} > + New
) } private async propertiesClicked(e: React.MouseEvent) { const {boardTree} = this.props const {activeView} = boardTree const selectProperties = boardTree.board.cardProperties OldMenu.shared.options = selectProperties.map((o) => { const isVisible = activeView.visiblePropertyIds.includes(o.id) return {id: o.id, name: o.name, type: 'switch', isOn: isVisible} }); OldMenu.shared.onMenuToggled = async (id: string, isOn: boolean) => { const property = selectProperties.find((o) => o.id === id) Utils.assertValue(property) Utils.log(`Toggle property ${property.name} ${isOn}`) let newVisiblePropertyIds = [] if (activeView.visiblePropertyIds.includes(id)) { newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== id) } else { newVisiblePropertyIds = [...activeView.visiblePropertyIds, id] } await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) }; OldMenu.shared.showAtElement(e.target as HTMLElement) } 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) } focusOnCardTitle(cardId: string) { const tableRowRef = this.cardIdToRowMap.get(cardId) Utils.log(`focusOnCardTitle, ${tableRowRef?.current ?? 'undefined'}`) tableRowRef?.current.focusOnTitle() } async addCard(show = false) { const {boardTree} = this.props const card = new Card() 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) } 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() } } searchChanged(text?: string) { this.props.setSearchText(text) } } export {TableComponent}