diff --git a/webapp/src/components/boardCard.tsx b/webapp/src/components/boardCard.tsx index 333da4049..a7cc96646 100644 --- a/webapp/src/components/boardCard.tsx +++ b/webapp/src/components/boardCard.tsx @@ -14,6 +14,7 @@ import {Utils} from '../utils' type BoardCardProps = { card: Card visiblePropertyTemplates: IPropertyTemplate[] + isSelected: boolean onClick?: (e: React.MouseEvent) => void onDragStart?: (e: React.DragEvent) => void onDragEnd?: (e: React.DragEvent) => void @@ -33,9 +34,11 @@ class BoardCard extends React.Component { const {card} = this.props const optionsButtonRef = React.createRef() const visiblePropertyTemplates = this.props.visiblePropertyTemplates || [] + const className = this.props.isSelected ? 'octo-board-card selected' : 'octo-board-card' + const element = (
{ - private draggedCard: Card + private draggedCards: Card[] = [] private draggedHeaderOption: IPropertyOption + private backgroundRef = React.createRef() private searchFieldRef = React.createRef() + private keydownHandler = (e: KeyboardEvent) => { + if (e.target !== document.body) { + return + } + + if (e.keyCode === 27) { + if (this.state.selectedCards.length > 0) { + this.setState({selectedCards: []}) + e.stopPropagation() + } + } + } + + componentDidMount() { + document.addEventListener('keydown', this.keydownHandler) + } + + componentWillUnmount() { + document.removeEventListener('keydown', this.keydownHandler) + } + constructor(props: Props) { super(props) - this.state = {isHoverOnCover: false, isSearching: Boolean(this.props.boardTree?.getSearchText()), viewMenu: false} + this.state = { + isHoverOnCover: false, + isSearching: Boolean(this.props.boardTree?.getSearchText()), + viewMenu: false, + selectedCards: [], + } } componentDidUpdate(prevPros: Props, prevState: State) { @@ -73,7 +101,13 @@ class BoardComponent extends React.Component { const hasSort = activeView.sortOptions.length > 0 return ( -
+
{ + this.backgroundClicked(e) + }} + > {this.state.shownCard && { card={card} visiblePropertyTemplates={visiblePropertyTemplates} key={card.id} - onClick={() => { - this.setState({shownCard: card}) + isSelected={this.state.selectedCards.includes(card)} + onClick={(e) => { + this.cardClicked(e, card) }} onDragStart={() => { - this.draggedCard = card + this.draggedCards = this.state.selectedCards.includes(card) ? this.state.selectedCards : [card] }} onDragEnd={() => { - this.draggedCard = undefined + this.draggedCards = [] }} />), )} @@ -364,14 +399,15 @@ class BoardComponent extends React.Component { card={card} visiblePropertyTemplates={visiblePropertyTemplates} key={card.id} - onClick={() => { - this.setState({shownCard: card}) + isSelected={this.state.selectedCards.includes(card)} + onClick={(e) => { + this.cardClicked(e, card) }} onDragStart={() => { - this.draggedCard = card + this.draggedCards = this.state.selectedCards.includes(card) ? this.state.selectedCards : [card] }} onDragEnd={() => { - this.draggedCard = undefined + this.draggedCards = [] }} />), )} @@ -389,6 +425,13 @@ class BoardComponent extends React.Component { ) } + private backgroundClicked(e: React.MouseEvent) { + if (this.state.selectedCards.length > 0) { + this.setState({selectedCards: []}) + e.stopPropagation() + } + } + async addCard(groupByValue?: string) { const {boardTree} = this.props const {activeView, board} = boardTree @@ -524,6 +567,22 @@ class BoardComponent extends React.Component { OldMenu.shared.showAtElement(e.target as HTMLElement) } + private cardClicked(e: React.MouseEvent, card: Card) { + if (e.shiftKey) { + let selectedCards = this.state.selectedCards.slice() + if (selectedCards.includes(card)) { + selectedCards = selectedCards.filter((o) => o != card) + } else { + selectedCards.push(card) + } + this.setState({selectedCards}) + } else { + this.setState({selectedCards: [], shownCard: card}) + } + + e.stopPropagation() + } + async addGroupClicked() { console.log('onAddGroupClicked') @@ -540,17 +599,19 @@ class BoardComponent extends React.Component { async onDropToColumn(option: IPropertyOption) { const {boardTree} = this.props - const {draggedCard, draggedHeaderOption} = this + const {draggedCards, draggedHeaderOption} = this const propertyValue = option ? option.value : undefined Utils.assertValue(mutator) Utils.assertValue(boardTree) - if (draggedCard) { - Utils.log(`ondrop. Card: ${draggedCard.title}, column: ${propertyValue}`) - const oldValue = draggedCard.properties[boardTree.groupByProperty.id] - if (propertyValue !== oldValue) { - await mutator.changePropertyValue(draggedCard, boardTree.groupByProperty.id, propertyValue, 'drag card') + if (draggedCards.length > 0) { + for (const draggedCard of draggedCards) { + Utils.log(`ondrop. Card: ${draggedCard.title}, column: ${propertyValue}`) + const oldValue = draggedCard.properties[boardTree.groupByProperty.id] + if (propertyValue !== oldValue) { + await mutator.changePropertyValue(draggedCard, boardTree.groupByProperty.id, propertyValue, 'drag card') + } } } else if (draggedHeaderOption) { Utils.log(`ondrop. Header option: ${draggedHeaderOption.value}, column: ${propertyValue}`) @@ -568,7 +629,7 @@ class BoardComponent extends React.Component { onSearchKeyDown(e: React.KeyboardEvent) { if (e.keyCode === 27) { // ESC: Clear search this.searchFieldRef.current.text = '' - this.setState({...this.state, isSearching: false}) + this.setState({isSearching: false}) this.props.setSearchText(undefined) e.preventDefault() } diff --git a/webapp/static/main.css b/webapp/static/main.css index 313ce6d5d..dcad2e56f 100644 --- a/webapp/static/main.css +++ b/webapp/static/main.css @@ -237,6 +237,10 @@ hr { transition: background 100ms ease-out 0s; } +.octo-board-card.selected { + background-color: rgba(90, 200, 255, 0.2); +} + .octo-board-card > div { margin-bottom: 3px; }