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:
parent
dbfeeed8ed
commit
b3dd307664
14 changed files with 580 additions and 65 deletions
|
@ -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",
|
||||
|
|
|
@ -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 || {})}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
90
webapp/src/components/table/tableGroup.tsx
Normal file
90
webapp/src/components/table/tableGroup.tsx
Normal 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
|
151
webapp/src/components/table/tableGroupHeaderRow.tsx
Normal file
151
webapp/src/components/table/tableGroupHeaderRow.tsx
Normal 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
|
|
@ -27,8 +27,4 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
margin-top: 48px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
76
webapp/src/components/table/tableRows.tsx
Normal file
76
webapp/src/components/table/tableRows.tsx
Normal 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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -241,6 +241,7 @@ class MutableBoardTree implements BoardTree {
|
|||
Utils.assertValue(property)
|
||||
}
|
||||
this.groupByProperty = property
|
||||
this.activeView.groupById = property?.id
|
||||
|
||||
this.groupCards()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue