GH-408 Implement Table Group (#463)

* initial checkin

* temporary commit

* most functionality working

* cleanup

* fixes for read-only mode

* implement drop on groups

* implement dnd card -> groupheader

* fix linter

* remove setting input size, set to 1st column width

* fix linter

* revert change

* add ungroup feature

* rework to handle fixed header row.

* fix for deleting group by property

* make falsy

* post merge fixes, handle multi-select

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Scott Bishel 2021-06-07 09:47:22 -05:00 committed by GitHub
parent dbfeeed8ed
commit b3dd307664
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 580 additions and 65 deletions

View file

@ -44,6 +44,7 @@
"FilterComponent.delete": "Delete",
"GalleryCard.delete": "Delete",
"GalleryCard.duplicate": "Duplicate",
"GroupBy.ungroup": "Ungroup",
"KanbanCard.delete": "Delete",
"KanbanCard.duplicate": "Duplicate",
"KanbanCard.untitled": "Untitled",

View file

@ -15,6 +15,7 @@ interface BoardView extends IBlock {
readonly visiblePropertyIds: readonly string[]
readonly visibleOptionIds: readonly string[]
readonly hiddenOptionIds: readonly string[]
readonly collapsedOptionIds: readonly string[]
readonly filter: FilterGroup
readonly cardOrder: readonly string[]
readonly columnWidths: Readonly<Record<string, number>>
@ -65,6 +66,13 @@ class MutableBoardView extends MutableBlock implements BoardView {
this.fields.hiddenOptionIds = value
}
get collapsedOptionIds(): string[] {
return this.fields.collapsedOptionIds
}
set collapsedOptionIds(value: string[]) {
this.fields.collapsedOptionIds = value
}
get filter(): FilterGroup {
return this.fields.filter
}
@ -95,6 +103,7 @@ class MutableBoardView extends MutableBlock implements BoardView {
this.visiblePropertyIds = block.fields?.visiblePropertyIds?.slice() || []
this.visibleOptionIds = block.fields?.visibleOptionIds?.slice() || []
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
this.collapsedOptionIds = block.fields?.collapsedOptionIds?.slice() || []
this.filter = new FilterGroup(block.fields?.filter)
this.cardOrder = block.fields?.cardOrder?.slice() || []
this.columnWidths = {...(block.fields?.columnWidths || {})}

View file

@ -164,7 +164,7 @@ class CenterPanel extends React.Component<Props, State> {
readonly={this.props.readonly}
cardIdToFocusOnRender={this.state.cardIdToFocusOnRender}
showCard={this.showCard}
addCard={(show) => this.addCard('', show)}
addCard={this.addCard}
onCardClicked={this.cardClicked}
intl={this.props.intl}
/>}
@ -212,7 +212,7 @@ class CenterPanel extends React.Component<Props, State> {
card.parentId = boardTree.board.id
card.rootId = boardTree.board.rootId
const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
if (activeView.viewType === 'board' && boardTree.groupByProperty) {
if ((activeView.viewType === 'board' || activeView.viewType === 'table') && boardTree.groupByProperty) {
if (groupByOptionId) {
propertiesThatMeetFilters[boardTree.groupByProperty.id] = groupByOptionId
} else {

View file

@ -1,4 +1,76 @@
.Table {
.table-row-container {
margin-top: 48px;
}
.octo-group-header-cell {
display: flex;
flex-shrink: 0;
align-items: center;
height: 50px;
margin-right: 15px;
margin-top: 15px;
vertical-align: middle;
&.narrow {
width: 220px;
}
> div {
margin-right: 8px;
&:last-child {
margin: 0;
}
}
.IconButton {
background-color: unset;
&:hover:not(.readonly) {
background-color: rgba(var(--body-color), 0.1);
}
&.readonly {
opacity: .5;
}
}
.Label {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 3px;
line-height: 20px;
margin-right: 5px;
color: rgba(var(--body-color), 1);
white-space: nowrap;
text-transform: none;
font-weight: normal;
font-size: 16px;
width: 100%;
input {
background: transparent;
width: 100%;
text-transform: none;
font-weight: normal;
font-size: 16px;
// line-height: 20px;
color: rgba(var(--body-color), 1);
}
}
> .Button {
&.IconButton:not(.readonly) {
cursor: pointer;
}
cursor: auto;
}
&.expanded {
.DisclosureTriangleIcon {
transform: rotate(90deg);
}
}
}
.octo-table-cell {
flex: 0 0 auto;
display: flex;
@ -85,6 +157,9 @@
flex-direction: row;
border-bottom: solid 1px rgba(var(--body-color), 0.09);
&.hidden {
display: none;
}
}
.octo-table-header {
@ -117,7 +192,6 @@
}
.MenuWrapper {
width: 100%;
max-width: calc(100% - 5px);
.Label {

View file

@ -1,10 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage, IntlShape} from 'react-intl'
import {useDrop, useDragLayer} from 'react-dnd'
import {IPropertyTemplate} from '../../blocks/board'
import {IPropertyOption, IPropertyTemplate} from '../../blocks/board'
import {MutableBoardView} from '../../blocks/boardView'
import {Card} from '../../blocks/card'
import {Constants} from '../../constants'
@ -17,7 +18,8 @@ import {OctoUtils} from './../../octoUtils'
import './table.scss'
import TableHeader from './tableHeader'
import TableRow from './tableRow'
import TableRows from './tableRows'
import TableGroup from './tableGroup'
type Props = {
boardTree: BoardTree
@ -26,13 +28,14 @@ type Props = {
cardIdToFocusOnRender: string
intl: IntlShape
showCard: (cardId?: string) => void
addCard: (show: boolean) => Promise<void>
addCard: (groupByOptionId?: string) => Promise<void>
onCardClicked: (e: React.MouseEvent, card: Card) => void
}
const Table = (props: Props) => {
const {boardTree} = props
const {board, cards, activeView} = boardTree
const {board, cards, activeView, visibleGroups} = boardTree
const isManualSort = activeView.sortOptions.length < 1
const {offset, resizingColumn} = useDragLayer((monitor) => {
if (monitor.getItemType() === 'horizontalGrip') {
@ -81,8 +84,7 @@ const Table = (props: Props) => {
if (!template) {
return
}
displayValue = (OctoUtils.propertyDisplayValue(card, card.properties[columnID], template!, props.intl) || '') as string
displayValue = (OctoUtils.propertyDisplayValue(card, card.properties[columnID], template, props.intl) || '') as string
if (template.type === 'select') {
displayValue = displayValue.toUpperCase()
}
@ -93,9 +95,6 @@ const Table = (props: Props) => {
}
})
if (longestSize === 0) {
return
}
const columnWidths = {...activeView.columnWidths}
columnWidths[columnID] = longestSize
const newView = new MutableBoardView(activeView)
@ -103,25 +102,19 @@ const Table = (props: Props) => {
mutator.updateBlock(newView, activeView, 'autosize column')
})
const onDropToCard = (srcCard: Card, dstCard: Card) => {
Utils.log(`onDropToCard: ${dstCard.title}`)
const {selectedCardIds} = props
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
// Update dstCard order
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.cards.map((o) => o.id)]))
const isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCard.id)
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
let destIndex = cardOrder.indexOf(dstCard.id)
if (isDraggingDown) {
destIndex += 1
const hideGroup = (groupById: string): void => {
const index : number = activeView.collapsedOptionIds.indexOf(groupById)
const newValue : string[] = [...activeView.collapsedOptionIds]
if (index > -1) {
newValue.splice(index, 1)
} else if (groupById !== '') {
newValue.push(groupById)
}
cardOrder.splice(destIndex, 0, ...draggedCardIds)
const newView = new MutableBoardView(activeView)
newView.collapsedOptionIds = newValue
mutator.performAsUndoGroup(async () => {
await mutator.changeViewCardOrder(activeView, cardOrder, description)
await mutator.updateBlock(newView, activeView, 'hide group')
})
}
@ -133,6 +126,92 @@ const Table = (props: Props) => {
await mutator.changePropertyTemplateOrder(board, template, destIndex >= 0 ? destIndex : 0)
}
const onDropToGroupHeader = async (option: IPropertyOption, dstOption?: IPropertyOption) => {
if (dstOption) {
Utils.log(`ondrop. Header target: ${dstOption.value}, source: ${option?.value}`)
// Move option to new index
const visibleOptionIds = boardTree.visibleGroups.map((o) => o.option.id)
const srcIndex = visibleOptionIds.indexOf(dstOption.id)
const destIndex = visibleOptionIds.indexOf(option.id)
visibleOptionIds.splice(srcIndex, 0, visibleOptionIds.splice(destIndex, 1)[0])
Utils.log(`ondrop. updated visibleoptionids: ${visibleOptionIds}`)
await mutator.changeViewVisibleOptionIds(activeView, visibleOptionIds)
}
}
const onDropToCard = (srcCard: Card, dstCard: Card) => {
Utils.log(`onDropToCard: ${dstCard.title}`)
onDropToGroup(srcCard, dstCard.properties[activeView.groupById!] as string, dstCard.id)
}
const onDropToGroup = (srcCard: Card, groupID: string, dstCardID: string) => {
Utils.log(`onDropToGroup: ${srcCard.title}`)
const {selectedCardIds} = props
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
if (activeView.groupById !== undefined) {
const orderedCards = boardTree.orderedCards()
const cardsById: {[key: string]: Card} = orderedCards.reduce((acc: {[key: string]: Card}, card: Card): {[key: string]: Card} => {
acc[card.id] = card
return acc
}, {})
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o])
mutator.performAsUndoGroup(async () => {
// Update properties of dragged cards
const awaits = []
for (const draggedCard of draggedCards) {
Utils.log(`draggedCard: ${draggedCard.title}, column: ${draggedCard.properties}`)
Utils.log(`droppedColumn: ${groupID}`)
const oldOptionId = draggedCard.properties[boardTree.groupByProperty!.id]
Utils.log(`ondrop. oldValue: ${oldOptionId}`)
if (groupID !== oldOptionId) {
awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, groupID, description))
}
}
await Promise.all(awaits)
})
}
// Update dstCard order
if (isManualSort) {
let cardOrder = Array.from(new Set([...activeView.cardOrder, ...boardTree.cards.map((o) => o.id)]))
if (dstCardID) {
const isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCardID)
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
let destIndex = cardOrder.indexOf(dstCardID)
if (isDraggingDown) {
destIndex += 1
}
cardOrder.splice(destIndex, 0, ...draggedCardIds)
} else {
// Find index of first group item
const firstCard = boardTree.orderedCards().find((card) => card.properties[activeView.groupById!] === groupID)
if (firstCard) {
const destIndex = cardOrder.indexOf(firstCard.id)
cardOrder.splice(destIndex, 0, ...draggedCardIds)
} else {
// if not found, this is the only item in group.
return
}
}
mutator.performAsUndoGroup(async () => {
await mutator.changeViewCardOrder(activeView, cardOrder, description)
})
}
}
const propertyNameChanged = async (option: IPropertyOption, text: string): Promise<void> => {
await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty!, option, text)
}
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
let titleSorted: 'up' | 'down' | 'none' = 'none'
if (titleSortOption) {
@ -193,43 +272,57 @@ const Table = (props: Props) => {
})}
</div>
{/* Rows, one per card */}
{/* Table header row */}
<div className='table-row-container'>
{activeView.groupById &&
visibleGroups.map((group) => {
return (
<TableGroup
key={group.option.id}
boardTree={boardTree}
group={group}
intl={props.intl}
readonly={props.readonly}
columnRefs={columnRefs}
selectedCardIds={props.selectedCardIds}
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
hideGroup={hideGroup}
addCard={props.addCard}
showCard={props.showCard}
propertyNameChanged={propertyNameChanged}
onCardClicked={props.onCardClicked}
onDropToGroupHeader={onDropToGroupHeader}
onDropToCard={onDropToCard}
onDropToGroup={onDropToGroup}
/>)
})
}
{cards.map((card) => {
const tableRow = (
<TableRow
key={card.id + card.updateAt}
{/* No Grouping, Rows, one per card */}
{!activeView.groupById &&
<TableRows
boardTree={boardTree}
card={card}
isSelected={props.selectedCardIds.includes(card.id)}
focusOnMount={props.cardIdToFocusOnRender === card.id}
onSaveWithEnter={() => {
if (cards.length > 0 && cards[cards.length - 1] === card) {
props.addCard(false)
}
}}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
props.onCardClicked(e, card)
}}
showCard={props.showCard}
readonly={props.readonly}
onDrop={onDropToCard}
offset={offset}
resizingColumn={resizingColumn}
columnRefs={columnRefs}
/>)
return tableRow
})}
cards={boardTree.cards}
selectedCardIds={props.selectedCardIds}
readonly={props.readonly}
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
intl={props.intl}
showCard={props.showCard}
addCard={props.addCard}
onCardClicked={props.onCardClicked}
onDrop={onDropToCard}
/>
}
</div>
{/* Add New row */}
<div className='octo-table-footer'>
{!props.readonly &&
{!props.readonly && !activeView.groupById &&
<div
className='octo-table-cell'
onClick={() => {
props.addCard(false)
props.addCard('')
}}
>
<FormattedMessage

View file

@ -0,0 +1,90 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React from 'react'
import {IntlShape} from 'react-intl'
import {useDrop} from 'react-dnd'
import {IPropertyOption} from '../../blocks/board'
import {Card} from '../../blocks/card'
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
import TableGroupHeaderRow from './tableGroupHeaderRow'
import TableRows from './tableRows'
type Props = {
boardTree: BoardTree
group: BoardTreeGroup
intl: IntlShape
readonly: boolean
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
selectedCardIds: string[]
cardIdToFocusOnRender: string
hideGroup: (groupByOptionId: string) => void
addCard: (groupByOptionId?: string) => Promise<void>
showCard: (cardId?: string) => void
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
onCardClicked: (e: React.MouseEvent, card: Card) => void
onDropToGroupHeader: (srcOption: IPropertyOption, dstOption?: IPropertyOption) => void
onDropToCard: (srcCard: Card, dstCard: Card) => void
onDropToGroup: (srcCard: Card, groupID: string, dstCardID: string) => void
}
const TableGroup = React.memo((props: Props): JSX.Element => {
const {boardTree, group, onDropToGroup} = props
const groupId = group.option.id
const [{isOver}, drop] = useDrop(() => ({
accept: 'card',
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
drop: (item: Card, monitor) => {
if (monitor.isOver({shallow: true})) {
onDropToGroup(item, groupId, '')
}
},
}), [onDropToGroup, groupId])
let className = 'octo-table-group'
if (isOver) {
className += ' dragover'
}
return (
<div
ref={drop}
className={className}
key={group.option.id}
>
<TableGroupHeaderRow
group={group}
boardTree={boardTree}
intl={props.intl}
hideGroup={props.hideGroup}
addCard={props.addCard}
readonly={props.readonly}
propertyNameChanged={props.propertyNameChanged}
onDrop={props.onDropToGroupHeader}
/>
{(group.cards.length > 0) &&
<TableRows
boardTree={boardTree}
columnRefs={props.columnRefs}
cards={group.cards}
selectedCardIds={props.selectedCardIds}
readonly={props.readonly}
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
intl={props.intl}
showCard={props.showCard}
addCard={props.addCard}
onCardClicked={props.onCardClicked}
onDrop={props.onDropToCard}
/>}
</div>
)
})
export default TableGroup

View file

@ -0,0 +1,151 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React, {useState, useEffect} from 'react'
import {FormattedMessage, IntlShape} from 'react-intl'
import {Constants} from '../../constants'
import {IPropertyOption} from '../../blocks/board'
import {useSortable} from '../../hooks/sortable'
import mutator from '../../mutator'
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
import Button from '../../widgets/buttons/button'
import IconButton from '../../widgets/buttons/iconButton'
import AddIcon from '../../widgets/icons/add'
import DeleteIcon from '../../widgets/icons/delete'
import DisclosureTriangle from '../../widgets/icons/disclosureTriangle'
import HideIcon from '../../widgets/icons/hide'
import OptionsIcon from '../../widgets/icons/options'
import Menu from '../../widgets/menu'
import MenuWrapper from '../../widgets/menuWrapper'
import Editable from '../../widgets/editable'
import Label from '../../widgets/label'
type Props = {
boardTree: BoardTree
group: BoardTreeGroup
intl: IntlShape
readonly: boolean
hideGroup: (groupByOptionId: string) => void
addCard: (groupByOptionId?: string) => Promise<void>
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
onDrop: (srcOption: IPropertyOption, dstOption?: IPropertyOption) => void
}
const TableGroupHeaderRow = React.memo((props: Props): JSX.Element => {
const {boardTree, intl, group} = props
const {activeView} = boardTree
const [groupTitle, setGroupTitle] = useState(group.option.value)
const [isDragging, isOver, groupHeaderRef] = useSortable('groupHeader', group.option, !props.readonly, props.onDrop)
useEffect(() => {
setGroupTitle(group.option.value)
}, [group.option.value])
let className = 'octo-group-header-cell'
if (isOver) {
className += ' dragover'
}
if (activeView.collapsedOptionIds.indexOf(group.option.id || 'undefined') < 0) {
className += ' expanded'
}
const columnWidth = (templateId: string): number => {
return Math.max(Constants.minColumnWidth, props.boardTree.activeView.columnWidths[templateId] || 0)
}
return (
<div
key={group.option.id + 'header'}
ref={groupHeaderRef}
style={{opacity: isDragging ? 0.5 : 1}}
className={className}
>
<div
className='octo-table-cell'
style={{width: columnWidth(Constants.titleColumnId)}}
>
<IconButton
icon={<DisclosureTriangle/>}
onClick={() => (props.readonly ? {} : props.hideGroup(group.option.id || 'undefined'))}
className={props.readonly ? 'readonly' : ''}
/>
{!group.option.id &&
<Label
title={intl.formatMessage({
id: 'BoardComponent.no-property-title',
defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
}, {property: boardTree.groupByProperty!.name})}
>
<FormattedMessage
id='BoardComponent.no-property'
defaultMessage='No {property}'
values={{
property: boardTree.groupByProperty!.name,
}}
/>
</Label>}
{group.option.id &&
<Label color={group.option.color}>
<Editable
value={groupTitle}
placeholderText='New Select'
onChange={setGroupTitle}
onSave={() => {
if (groupTitle.trim() === '') {
setGroupTitle(group.option.value)
}
props.propertyNameChanged(group.option, groupTitle)
}}
onCancel={() => {
setGroupTitle(group.option.value)
}}
readonly={props.readonly || !group.option.id}
spellCheck={true}
/>
</Label>}
</div>
<Button>{`${group.cards.length}`}</Button>
{!props.readonly &&
<>
<MenuWrapper>
<IconButton icon={<OptionsIcon/>}/>
<Menu>
<Menu.Text
id='hide'
icon={<HideIcon/>}
name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
onClick={() => mutator.hideViewColumn(activeView, group.option.id || '')}
/>
{group.option.id &&
<>
<Menu.Text
id='delete'
icon={<DeleteIcon/>}
name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
/>
<Menu.Separator/>
{Constants.menuColors.map((color) => (
<Menu.Color
key={color.id}
id={color.id}
name={color.name}
onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
/>
))}
</>}
</Menu>
</MenuWrapper>
<IconButton
icon={<AddIcon/>}
onClick={() => props.addCard(group.option.id)}
/>
</>
}
</div>
)
})
export default TableGroupHeaderRow

View file

@ -27,8 +27,4 @@
display: block;
}
}
&:nth-child(2) {
margin-top: 48px;
}
}

View file

@ -37,7 +37,8 @@ const TableRow = React.memo((props: Props) => {
const [title, setTitle] = useState(props.card.title)
const {card} = props
const isManualSort = activeView.sortOptions.length < 1
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly && isManualSort, props.onDrop)
const isGrouped = Boolean(activeView.groupById)
const [isDragging, isOver, cardRef] = useSortable('card', card, !props.readonly && (isManualSort || isGrouped), props.onDrop)
useEffect(() => {
if (props.focusOnMount) {
@ -56,6 +57,13 @@ const TableRow = React.memo((props: Props) => {
if (isOver) {
className += ' dragover'
}
if (isGrouped) {
const groupID = activeView.groupById || ''
const groupValue = card.properties[groupID] as string || 'undefined'
if (activeView.collapsedOptionIds.indexOf(groupValue) > -1) {
className += ' hidden'
}
}
if (!columnRefs.get(Constants.titleColumnId)) {
columnRefs.set(Constants.titleColumnId, React.createRef())
@ -70,7 +78,6 @@ const TableRow = React.memo((props: Props) => {
>
{/* Name / title */}
<div
className='octo-table-cell title-cell'
id='mainBoardHeader'

View file

@ -0,0 +1,76 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {IntlShape} from 'react-intl'
import {useDragLayer} from 'react-dnd'
import {Card} from '../../blocks/card'
import {BoardTree} from '../../viewModel/boardTree'
import './table.scss'
import TableRow from './tableRow'
type Props = {
boardTree: BoardTree
columnRefs: Map<string, React.RefObject<HTMLDivElement>>
cards: readonly Card[]
selectedCardIds: string[]
readonly: boolean
cardIdToFocusOnRender: string
intl: IntlShape
showCard: (cardId?: string) => void
addCard: (groupByOptionId?: string) => Promise<void>
onCardClicked: (e: React.MouseEvent, card: Card) => void
onDrop: (srcCard: Card, dstCard: Card) => void}
const TableRows = (props: Props) => {
const {boardTree, cards} = props
const {activeView} = boardTree
const {offset, resizingColumn} = useDragLayer((monitor) => {
if (monitor.getItemType() === 'horizontalGrip') {
return {
offset: monitor.getDifferenceFromInitialOffset()?.x || 0,
resizingColumn: monitor.getItem()?.id,
}
}
return {
offset: 0,
resizingColumn: '',
}
})
return (
<>
{cards.map((card) => {
const tableRow = (
<TableRow
key={card.id + card.updateAt}
boardTree={boardTree}
card={card}
isSelected={props.selectedCardIds.includes(card.id)}
focusOnMount={props.cardIdToFocusOnRender === card.id}
onSaveWithEnter={() => {
if (cards.length > 0 && cards[cards.length - 1] === card) {
props.addCard(activeView.groupById ? card.properties[activeView.groupById!] as string : '')
}
}}
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
props.onCardClicked(e, card)
}}
showCard={props.showCard}
readonly={props.readonly}
onDrop={props.onDrop}
offset={offset}
resizingColumn={resizingColumn}
columnRefs={props.columnRefs}
/>)
return tableRow
})}
</>
)
}
export default TableRows

View file

@ -41,7 +41,7 @@ const ViewHeader = React.memo((props: Props) => {
const {boardTree, showView} = props
const {board, activeView} = boardTree
const withGroupBy = activeView.viewType === 'board'
const withGroupBy = activeView.viewType === 'board' || activeView.viewType === 'table'
const [viewTitle, setViewTitle] = useState(activeView.title)

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {FormattedMessage, useIntl} from 'react-intl'
import {IPropertyTemplate} from '../../blocks/board'
import {BoardView} from '../../blocks/boardView'
@ -20,6 +20,7 @@ type Props = {
const ViewHeaderGroupByMenu = React.memo((props: Props) => {
const {properties, activeView, groupByPropertyName} = props
const intl = useIntl()
return (
<MenuWrapper>
<Button>
@ -39,6 +40,22 @@ const ViewHeaderGroupByMenu = React.memo((props: Props) => {
/>
</Button>
<Menu>
{activeView.viewType === 'table' && activeView.groupById &&
<>
<Menu.Text
key={'ungroup'}
id={''}
name={intl.formatMessage({id: 'GroupBy.ungroup', defaultMessage: 'Ungroup'})}
rightIcon={activeView.groupById === '' ? <CheckIcon/> : undefined}
onClick={(id) => {
if (activeView.groupById === id) {
return
}
mutator.changeViewGroupById(activeView, id)
}}
/>
<Menu.Separator/>
</>}
{properties.filter((o: IPropertyTemplate) => o.type === 'select').map((option: IPropertyTemplate) => (
<Menu.Text
key={option.id}

View file

@ -82,10 +82,10 @@ class Utils {
Array.from(children).forEach((element) => {
switch (element.className) {
case IconClass:
case SpacerClass:
case HorizontalGripClass:
myResults.padding += element.clientWidth
break
case SpacerClass:
case OpenButtonClass:
break
default: {

View file

@ -241,6 +241,7 @@ class MutableBoardTree implements BoardTree {
Utils.assertValue(property)
}
this.groupByProperty = property
this.activeView.groupById = property?.id
this.groupCards()
}