mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-21 13:38:56 +02:00
virtual list for grouped table
This commit is contained in:
parent
19007c486a
commit
77d7e5f6bc
25
webapp/package-lock.json
generated
25
webapp/package-lock.json
generated
@ -49,6 +49,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^5.2.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-vtree": "^2.0.4",
|
||||
"react-window": "^1.8.8",
|
||||
"trim-newlines": "^4.0.2"
|
||||
},
|
||||
@ -2812,7 +2813,6 @@
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@ -14109,6 +14109,20 @@
|
||||
"react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-vtree": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-vtree/-/react-vtree-2.0.4.tgz",
|
||||
"integrity": "sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react-window": "^1.8.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-window": "^1.8.5"
|
||||
}
|
||||
},
|
||||
"node_modules/react-window": {
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||
@ -19653,7 +19667,6 @@
|
||||
"version": "1.8.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.5.tgz",
|
||||
"integrity": "sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@ -28192,6 +28205,14 @@
|
||||
"integrity": "sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-vtree": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-vtree/-/react-vtree-2.0.4.tgz",
|
||||
"integrity": "sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.11.0"
|
||||
}
|
||||
},
|
||||
"react-window": {
|
||||
"version": "1.8.8",
|
||||
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz",
|
||||
|
@ -66,6 +66,7 @@
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-select": "^5.2.2",
|
||||
"react-virtualized-auto-sizer": "^1.0.7",
|
||||
"react-vtree": "^2.0.4",
|
||||
"react-window": "^1.8.8",
|
||||
"trim-newlines": "^4.0.2"
|
||||
},
|
||||
|
@ -11,9 +11,9 @@
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
height: 50px;
|
||||
height: 44px;
|
||||
margin-right: 15px;
|
||||
margin-top: 15px;
|
||||
// margin-top: 15px;
|
||||
vertical-align: middle;
|
||||
border-bottom: solid 1px rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
|
@ -1,18 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useCallback} from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import { FormattedMessage } from 'react-intl'
|
||||
import {
|
||||
FixedSizeTree,
|
||||
FixedSizeNodeData,
|
||||
FixedSizeNodeComponentProps,
|
||||
} from 'react-vtree';
|
||||
import { FixedSizeList, ListChildComponentProps } from 'react-window'
|
||||
import AutoSizer from 'react-virtualized-auto-sizer'
|
||||
|
||||
import {IPropertyOption, IPropertyTemplate, Board, BoardGroup} from '../../blocks/board'
|
||||
import {createBoardView, BoardView} from '../../blocks/boardView'
|
||||
import {Card} from '../../blocks/card'
|
||||
import {Constants, Permission} from '../../constants'
|
||||
import { IPropertyOption, IPropertyTemplate, Board, BoardGroup } from '../../blocks/board'
|
||||
import { createBoardView, BoardView } from '../../blocks/boardView'
|
||||
import { Card } from '../../blocks/card'
|
||||
import { Constants, Permission } from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {useAppDispatch} from '../../store/hooks'
|
||||
import {updateView} from '../../store/views'
|
||||
import {useHasCurrentBoardPermissions} from '../../hooks/permissions'
|
||||
import { Utils } from '../../utils'
|
||||
import { useAppDispatch } from '../../store/hooks'
|
||||
import { updateView } from '../../store/views'
|
||||
import { useHasCurrentBoardPermissions } from '../../hooks/permissions'
|
||||
|
||||
import BoardPermissionGate from '../permissions/boardPermissionGate'
|
||||
|
||||
@ -22,7 +29,8 @@ import HiddenCardCount from '../../components/hiddenCardCount/hiddenCardCount'
|
||||
|
||||
import TableHeaders from './tableHeaders'
|
||||
import TableRows from './tableRows'
|
||||
import TableGroup from './tableGroup'
|
||||
import TableRow from './tableRow'
|
||||
import TableGroupHeaderRow from './tableGroupHeaderRow'
|
||||
import CalculationRow from './calculation/calculationRow'
|
||||
import {ColumnResizeProvider} from './tableColumnResizeContext'
|
||||
|
||||
@ -43,15 +51,29 @@ type Props = {
|
||||
showHiddenCardCountNotification: (show: boolean) => void
|
||||
}
|
||||
|
||||
type TreeData = FixedSizeNodeData &
|
||||
{
|
||||
isCard: boolean;
|
||||
isLeaf: boolean;
|
||||
node: BoardGroup | Card;
|
||||
};
|
||||
|
||||
type StackElement = {
|
||||
nestingLevel: number;
|
||||
node: Card | BoardGroup;
|
||||
isLastCard: boolean;
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
const Table = (props: Props): JSX.Element => {
|
||||
const {board, cards, activeView, visibleGroups, groupByProperty, views, hiddenCardsCount} = props
|
||||
const { board, cards, activeView, visibleGroups, groupByProperty, views, hiddenCardsCount } = props
|
||||
const isManualSort = activeView.fields.sortOptions?.length === 0
|
||||
const canEditBoardProperties = useHasCurrentBoardPermissions([Permission.ManageBoardProperties])
|
||||
const canEditCards = useHasCurrentBoardPermissions([Permission.ManageBoardCards])
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const resizeColumn = useCallback(async (columnId: string, width: number) => {
|
||||
const columnWidths = {...activeView.fields.columnWidths}
|
||||
const columnWidths = { ...activeView.fields.columnWidths }
|
||||
const newWidth = Math.max(Constants.minColumnWidth, width)
|
||||
if (newWidth !== columnWidths[columnId]) {
|
||||
Utils.log(`Resize of column finished: prev=${columnWidths[columnId]}, new=${newWidth}`)
|
||||
@ -108,7 +130,7 @@ const Table = (props: Props): JSX.Element => {
|
||||
|
||||
const onDropToGroup = useCallback((srcCard: Card, groupID: string, dstCardID: string) => {
|
||||
Utils.log(`onDropToGroup: ${srcCard.title}`)
|
||||
const {selectedCardIds} = props
|
||||
const { selectedCardIds } = props
|
||||
|
||||
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
|
||||
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
|
||||
@ -170,6 +192,126 @@ const Table = (props: Props): JSX.Element => {
|
||||
await mutator.changePropertyOptionValue(board.id, board.cardProperties, groupByProperty!, option, text)
|
||||
}, [board, groupByProperty])
|
||||
|
||||
const onClickRow = useCallback((e: React.MouseEvent<HTMLDivElement>, card: Card) => {
|
||||
props.onCardClicked(e, card)
|
||||
}, [props.onCardClicked])
|
||||
|
||||
const Node = ({data: {node, isCard, isLeaf}, style, toggle, isOpen}: FixedSizeNodeComponentProps<TreeData>) => {
|
||||
if (isCard) {
|
||||
const card = node as Card;
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
>
|
||||
<TableRow
|
||||
key={card.id + card.updateAt}
|
||||
board={board}
|
||||
columnWidths={activeView.fields.columnWidths}
|
||||
isManualSort={activeView.fields.sortOptions.length === 0}
|
||||
groupById={activeView.fields.groupById}
|
||||
visiblePropertyIds={activeView.fields.visiblePropertyIds}
|
||||
collapsedOptionIds={activeView.fields.collapsedOptionIds}
|
||||
card={card}
|
||||
addCard={props.addCard}
|
||||
isSelected={props.selectedCardIds.includes(card.id)}
|
||||
focusOnMount={props.cardIdToFocusOnRender === card.id}
|
||||
isLastCard={isLeaf}
|
||||
onClick={onClickRow}
|
||||
showCard={props.showCard}
|
||||
readonly={props.readonly}
|
||||
onDrop={onDropToCard}
|
||||
/>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
const group = node as BoardGroup;
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
>
|
||||
<TableGroupHeaderRow
|
||||
group={group}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
groupByProperty={groupByProperty}
|
||||
hideGroup={hideGroup}
|
||||
addCard={props.addCard}
|
||||
readonly={props.readonly}
|
||||
propertyNameChanged={propertyNameChanged}
|
||||
onDrop={onDropToGroupHeader}
|
||||
key={group.option.id}
|
||||
onDropToGroup={onDropToGroup}
|
||||
groupToggle={toggle}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const treeWalker = useCallback(
|
||||
function* treeWalker(
|
||||
refresh: boolean,
|
||||
): Generator<TreeData | string | symbol, void, boolean> {
|
||||
const stack: StackElement[] = [];
|
||||
|
||||
for (let i = 0; i < visibleGroups.length; i++) {
|
||||
stack.push({
|
||||
nestingLevel: 0,
|
||||
node: visibleGroups[i],
|
||||
groupId: visibleGroups[i].option.id,
|
||||
isLastCard: false,
|
||||
});
|
||||
}
|
||||
|
||||
while (stack.length !== 0) {
|
||||
const {node, nestingLevel, isLastCard, groupId} = stack.pop()!;
|
||||
let isOpened = null
|
||||
let group = null
|
||||
let card = null
|
||||
const isVisible = activeView.fields.collapsedOptionIds?.indexOf(groupId) === -1
|
||||
|
||||
if (nestingLevel === 0) {
|
||||
group = node as BoardGroup
|
||||
const id = group.option.id;
|
||||
isOpened = yield refresh ?
|
||||
{
|
||||
id,
|
||||
isLeaf: group.cards.length === 0,
|
||||
isOpenByDefault: isVisible,
|
||||
node,
|
||||
isCard: false,
|
||||
}
|
||||
: id;
|
||||
} else {
|
||||
card = node as Card
|
||||
const id = card.id;
|
||||
isOpened = yield refresh
|
||||
? {
|
||||
id: card.id+card.updateAt,
|
||||
isCard: true,
|
||||
isOpenByDefault: isVisible,
|
||||
node,
|
||||
isLeaf: isLastCard,
|
||||
}
|
||||
: id;
|
||||
}
|
||||
|
||||
if (group && group.cards.length !== 0 && isOpened) {
|
||||
for (let i = group.cards.length - 1; i >= 0; i--) {
|
||||
stack.push({
|
||||
nestingLevel: nestingLevel + 1,
|
||||
node: group.cards[i],
|
||||
groupId: group.option.id,
|
||||
isLastCard: i === (group.cards.length - 1)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
, [visibleGroups, activeView.fields.collapsedOptionIds])
|
||||
|
||||
|
||||
return (
|
||||
<div className='Table'>
|
||||
<ColumnResizeProvider
|
||||
@ -188,63 +330,54 @@ const Table = (props: Props): JSX.Element => {
|
||||
{/* Table rows */}
|
||||
<div className='table-row-container'>
|
||||
{activeView.fields.groupById &&
|
||||
visibleGroups.map((group) => {
|
||||
return (
|
||||
<TableGroup
|
||||
key={group.option.id}
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
groupByProperty={groupByProperty}
|
||||
group={group}
|
||||
readonly={props.readonly || !canEditCards}
|
||||
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}
|
||||
/>)
|
||||
})
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<FixedSizeTree
|
||||
height={height}
|
||||
treeWalker={treeWalker}
|
||||
itemSize={44}
|
||||
width={'100%'}
|
||||
>
|
||||
{Node}
|
||||
</FixedSizeTree>
|
||||
)}
|
||||
</AutoSizer>
|
||||
}
|
||||
|
||||
{/* No Grouping, Rows, one per card */}
|
||||
{!activeView.fields.groupById &&
|
||||
<TableRows
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
selectedCardIds={props.selectedCardIds}
|
||||
readonly={props.readonly || !canEditCards}
|
||||
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
|
||||
showCard={props.showCard}
|
||||
addCard={props.addCard}
|
||||
onCardClicked={props.onCardClicked}
|
||||
onDrop={onDropToCard}
|
||||
useVirtualizedList={true}
|
||||
/>
|
||||
<TableRows
|
||||
board={board}
|
||||
activeView={activeView}
|
||||
cards={cards}
|
||||
selectedCardIds={props.selectedCardIds}
|
||||
readonly={props.readonly || !canEditCards}
|
||||
cardIdToFocusOnRender={props.cardIdToFocusOnRender}
|
||||
showCard={props.showCard}
|
||||
addCard={props.addCard}
|
||||
onCardClicked={props.onCardClicked}
|
||||
onDrop={onDropToCard}
|
||||
useVirtualizedList={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Add New row */}
|
||||
<div className='octo-table-footer'>
|
||||
{!props.readonly && !activeView.fields.groupById &&
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
props.addCard('')
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
</BoardPermissionGate>
|
||||
<BoardPermissionGate permissions={[Permission.ManageBoardCards]}>
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
props.addCard('')
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
</BoardPermissionGate>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -258,10 +391,10 @@ const Table = (props: Props): JSX.Element => {
|
||||
</ColumnResizeProvider>
|
||||
|
||||
{hiddenCardsCount > 0 &&
|
||||
<HiddenCardCount
|
||||
showHiddenCardNotification={props.showHiddenCardCountNotification}
|
||||
hiddenCardsCount={hiddenCardsCount}
|
||||
/>}
|
||||
<HiddenCardCount
|
||||
showHiddenCardNotification={props.showHiddenCardCountNotification}
|
||||
hiddenCardsCount={hiddenCardsCount}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ const TableGroup = (props: Props): JSX.Element => {
|
||||
onDrop={props.onDropToGroupHeader}
|
||||
key={group.option.id}
|
||||
onDropToGroup={onDropToGroup}
|
||||
groupToggle={() => {}}
|
||||
/>
|
||||
|
||||
{(group.cards.length > 0) &&
|
||||
@ -63,8 +64,7 @@ const TableGroup = (props: Props): JSX.Element => {
|
||||
onDrop={props.onDropToCard}
|
||||
useVirtualizedList={false}
|
||||
/>}
|
||||
</>
|
||||
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -72,6 +72,7 @@ test('should match snapshot, no groups', async () => {
|
||||
options: [{id: 'property1', value: 'Property 1', color: ''}],
|
||||
}}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
@ -91,6 +92,7 @@ test('should match snapshot with Group', async () => {
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
@ -110,6 +112,7 @@ test('should match snapshot on read only', async () => {
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
@ -134,6 +137,7 @@ test('should match snapshot, hide group', async () => {
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
@ -163,6 +167,7 @@ test('should match snapshot, add new', async () => {
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
@ -190,6 +195,7 @@ test('should match snapshot, edit title', async () => {
|
||||
propertyNameChanged={jest.fn()}
|
||||
onDrop={jest.fn()}
|
||||
onDropToGroup={jest.fn()}
|
||||
groupToggle={jest.fn()}
|
||||
/>
|
||||
</Wrapper>,
|
||||
)
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
/* eslint-disable max-lines */
|
||||
import React, {useState, useEffect, useRef} from 'react'
|
||||
import React, {useState, useEffect, useRef, useCallback} from 'react'
|
||||
import {FormattedMessage, useIntl} from 'react-intl'
|
||||
import {useDrag, useDrop} from 'react-dnd'
|
||||
|
||||
@ -35,6 +35,7 @@ type Props = {
|
||||
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
|
||||
onDrop: (srcOption: IPropertyOption, dstOption?: IPropertyOption) => void
|
||||
onDropToGroup: (srcCard: Card, groupID: string, dstCardID: string) => void
|
||||
groupToggle: () => void
|
||||
}
|
||||
|
||||
const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
@ -86,6 +87,14 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
|
||||
const canEditOption = groupByProperty?.type !== 'person' && group.option.id
|
||||
|
||||
const toggleGroup = useCallback(() => {
|
||||
if (props.readonly) {
|
||||
return
|
||||
}
|
||||
props.hideGroup(group.option.id || 'undefined')
|
||||
props.groupToggle()
|
||||
}, [props.readonly, group.option.id, props.groupToggle, props.hideGroup])
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.option.id + 'header'}
|
||||
@ -103,7 +112,7 @@ const TableGroupHeaderRow = (props: Props): JSX.Element => {
|
||||
<CompassIcon
|
||||
icon='menu-right'
|
||||
/>}
|
||||
onClick={() => (props.readonly ? {} : props.hideGroup(group.option.id || 'undefined'))}
|
||||
onClick={toggleGroup}
|
||||
className={`octo-table-cell__expand ${props.readonly ? 'readonly' : ''}`}
|
||||
/>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user