mirror of
https://github.com/mattermost/focalboard.git
synced 2025-02-01 19:14:35 +02:00
Migrating all drag and drop into react-dnd
This commit is contained in:
parent
3aecc91fb5
commit
13dbecc823
57
webapp/package-lock.json
generated
57
webapp/package-lock.json
generated
@ -1646,6 +1646,21 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@react-dnd/asap": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
|
||||
"integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
|
||||
},
|
||||
"@react-dnd/invariant": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
|
||||
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
|
||||
},
|
||||
"@react-dnd/shallowequal": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
|
||||
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz",
|
||||
@ -4331,6 +4346,16 @@
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"dnd-core": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.0.tgz",
|
||||
"integrity": "sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA==",
|
||||
"requires": {
|
||||
"@react-dnd/asap": "^4.0.0",
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"redux": "^4.0.5"
|
||||
}
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -10185,6 +10210,26 @@
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"react-dnd": {
|
||||
"version": "14.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-14.0.2.tgz",
|
||||
"integrity": "sha512-JoEL78sBCg8SzjOKMlkR70GWaPORudhWuTNqJ56lb2P8Vq0eM2+er3ZrMGiSDhOmzaRPuA9SNBz46nHCrjn11A==",
|
||||
"requires": {
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"@react-dnd/shallowequal": "^2.0.0",
|
||||
"dnd-core": "14.0.0",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"hoist-non-react-statics": "^3.3.2"
|
||||
}
|
||||
},
|
||||
"react-dnd-html5-backend": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-14.0.0.tgz",
|
||||
"integrity": "sha512-2wAQqRFC1hbRGmk6+dKhOXsyQQOn3cN8PSZyOUeOun9J8t3tjZ7PS2+aFu7CVu2ujMDwTJR3VTwZh8pj2kCv7g==",
|
||||
"requires": {
|
||||
"dnd-core": "14.0.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz",
|
||||
@ -10428,6 +10473,15 @@
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"redux": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.0.5.tgz",
|
||||
"integrity": "sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"symbol-observable": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.7",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
|
||||
@ -11677,8 +11731,7 @@
|
||||
"symbol-observable": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==",
|
||||
"optional": true
|
||||
"integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ=="
|
||||
},
|
||||
"symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
|
@ -25,6 +25,8 @@
|
||||
"marked": ">=2.0.1",
|
||||
"nanoevents": "^5.1.13",
|
||||
"react": "^17.0.2",
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-hot-keys": "^2.6.2",
|
||||
"react-hotkeys-hook": "^3.3.0",
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom'
|
||||
import {DndProvider} from 'react-dnd'
|
||||
import {HTML5Backend} from 'react-dnd-html5-backend'
|
||||
|
||||
import {FlashMessages} from './components/flashMessages'
|
||||
import {getCurrentLanguage, getMessages, storeLanguage} from './i18n'
|
||||
@ -51,78 +53,80 @@ export default class App extends React.PureComponent<unknown, State> {
|
||||
locale={this.state.language}
|
||||
messages={getMessages(this.state.language)}
|
||||
>
|
||||
<UserContext.Provider value={this.state.user}>
|
||||
<FlashMessages milliseconds={2000}/>
|
||||
<Router forceRefresh={true}>
|
||||
<div id='frame'>
|
||||
<div id='main'>
|
||||
<Switch>
|
||||
<Route path='/error'>
|
||||
<ErrorPage/>
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage/>
|
||||
</Route>
|
||||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/change_password'>
|
||||
<ChangePasswordPage/>
|
||||
</Route>
|
||||
<Route path='/shared'>
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<UserContext.Provider value={this.state.user}>
|
||||
<FlashMessages milliseconds={2000}/>
|
||||
<Router forceRefresh={true}>
|
||||
<div id='frame'>
|
||||
<div id='main'>
|
||||
<Switch>
|
||||
<Route path='/error'>
|
||||
<ErrorPage/>
|
||||
</Route>
|
||||
<Route path='/login'>
|
||||
<LoginPage/>
|
||||
</Route>
|
||||
<Route path='/register'>
|
||||
<RegisterPage/>
|
||||
</Route>
|
||||
<Route path='/change_password'>
|
||||
<ChangePasswordPage/>
|
||||
</Route>
|
||||
<Route path='/shared'>
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='/board'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/shared'
|
||||
render={({match}) => {
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='/board'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
<Route
|
||||
path='/workspace/:workspaceId/'
|
||||
render={({match}) => {
|
||||
if (this.state.initialLoad && !this.state.user) {
|
||||
const redirectUrl = `/workspace/${match.params.workspaceId}/`
|
||||
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/shared'
|
||||
render={({match}) => {
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
readonly={true}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route
|
||||
path='/workspace/:workspaceId/'
|
||||
render={({match}) => {
|
||||
if (this.state.initialLoad && !this.state.user) {
|
||||
const redirectUrl = `/workspace/${match.params.workspaceId}/`
|
||||
const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}`
|
||||
return <Redirect to={loginUrl}/>
|
||||
}
|
||||
return (
|
||||
<BoardPage
|
||||
workspaceId={match.params.workspaceId}
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Route path='/'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Route path='/'>
|
||||
{this.state.initialLoad && !this.state.user && <Redirect to='/login'/>}
|
||||
<BoardPage
|
||||
workspaceId='0'
|
||||
setLanguage={this.setAndStoreLanguage}
|
||||
/>
|
||||
</Route>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Router>
|
||||
</UserContext.Provider>
|
||||
</Router>
|
||||
</UserContext.Provider>
|
||||
</DndProvider>
|
||||
</IntlProvider>
|
||||
)
|
||||
}
|
||||
|
@ -78,9 +78,6 @@ class Kanban extends React.Component<Props, State> {
|
||||
readonly={this.props.readonly}
|
||||
propertyNameChanged={this.propertyNameChanged}
|
||||
onDropToColumn={this.onDropToColumn}
|
||||
setDraggedHeaderOption={(draggedHeaderOption?: IPropertyOption) => {
|
||||
this.setState({draggedHeaderOption})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -120,8 +117,7 @@ class Kanban extends React.Component<Props, State> {
|
||||
{visibleGroups.map((group) => (
|
||||
<KanbanColumn
|
||||
key={group.option.id || 'empty'}
|
||||
isDropZone={!isManualSort || group.cards.length < 1}
|
||||
onDrop={() => this.onDropToColumn(group.option)}
|
||||
onDrop={(card: Card) => this.onDropToColumn(group.option, card)}
|
||||
>
|
||||
{group.cards.map((card) => (
|
||||
<KanbanCard
|
||||
@ -133,21 +129,7 @@ class Kanban extends React.Component<Props, State> {
|
||||
onClick={(e) => {
|
||||
this.props.onCardClicked(e, card)
|
||||
}}
|
||||
onDragStart={() => {
|
||||
if (this.props.selectedCardIds.includes(card.id)) {
|
||||
this.setState({draggedCards: this.props.selectedCardIds.map((id) => boardTree.allCards.find((o) => o.id === id)!)})
|
||||
} else {
|
||||
this.setState({draggedCards: [card]})
|
||||
}
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.setState({draggedCards: []})
|
||||
}}
|
||||
|
||||
isDropZone={isManualSort}
|
||||
onDrop={() => {
|
||||
this.onDropToCard(card)
|
||||
}}
|
||||
onDrop={this.onDropToCard}
|
||||
/>
|
||||
))}
|
||||
{!this.props.readonly &&
|
||||
@ -176,8 +158,7 @@ class Kanban extends React.Component<Props, State> {
|
||||
boardTree={boardTree}
|
||||
intl={this.props.intl}
|
||||
readonly={this.props.readonly}
|
||||
onDropToColumn={this.onDropToColumn}
|
||||
hasDraggedCards={this.state.draggedCards.length > 0}
|
||||
onDrop={(card: Card) => this.onDropToColumn(group.option, card)}
|
||||
/>
|
||||
))}
|
||||
</div>}
|
||||
@ -206,14 +187,24 @@ class Kanban extends React.Component<Props, State> {
|
||||
await mutator.insertPropertyOption(boardTree, boardTree.groupByProperty!, option, 'add group')
|
||||
}
|
||||
|
||||
private onDropToColumn = async (option: IPropertyOption) => {
|
||||
const {boardTree} = this.props
|
||||
const {draggedCards, draggedHeaderOption} = this.state
|
||||
private onDropToColumn = async (option: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => {
|
||||
const {boardTree, selectedCardIds} = this.props
|
||||
const optionId = option ? option.id : undefined
|
||||
|
||||
let draggedCardIds = selectedCardIds
|
||||
if (card) {
|
||||
draggedCardIds = Array.from(new Set(selectedCardIds).add(card.id))
|
||||
}
|
||||
|
||||
Utils.assertValue(boardTree)
|
||||
|
||||
if (draggedCards.length > 0) {
|
||||
if (draggedCardIds.length > 0) {
|
||||
const orderedCards = boardTree.orderedCards()
|
||||
const cardsById: {[key: string]: Card} = orderedCards.reduce((acc: {[key: string]: Card}, c: Card): {[key: string]: Card} => {
|
||||
acc[c.id] = c
|
||||
return acc
|
||||
}, {})
|
||||
const draggedCards: Card[] = draggedCardIds.map((o: string) => cardsById[o])
|
||||
await mutator.performAsUndoGroup(async () => {
|
||||
const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card'
|
||||
const awaits = []
|
||||
@ -226,14 +217,14 @@ class Kanban extends React.Component<Props, State> {
|
||||
}
|
||||
await Promise.all(awaits)
|
||||
})
|
||||
} else if (draggedHeaderOption) {
|
||||
Utils.log(`ondrop. Header option: ${draggedHeaderOption.value}, column: ${option?.value}`)
|
||||
} else if (dstOption) {
|
||||
Utils.log(`ondrop. Header option: ${dstOption.value}, column: ${option?.value}`)
|
||||
|
||||
// Move option to new index
|
||||
const visibleOptionIds = boardTree.visibleGroups.map((o) => o.option.id)
|
||||
|
||||
const {activeView} = boardTree
|
||||
const srcIndex = visibleOptionIds.indexOf(draggedHeaderOption.id)
|
||||
const srcIndex = visibleOptionIds.indexOf(dstOption.id)
|
||||
const destIndex = visibleOptionIds.indexOf(option.id)
|
||||
|
||||
visibleOptionIds.splice(destIndex, 0, visibleOptionIds.splice(srcIndex, 1)[0])
|
||||
@ -242,28 +233,30 @@ class Kanban extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
|
||||
private async onDropToCard(card: Card) {
|
||||
Utils.log(`onDropToCard: ${card.title}`)
|
||||
const {boardTree} = this.props
|
||||
private onDropToCard = async (srcCard: Card, dstCard: Card) => {
|
||||
Utils.log(`onDropToCard: ${dstCard.title}`)
|
||||
const {boardTree, selectedCardIds} = this.props
|
||||
const {activeView} = boardTree
|
||||
const {draggedCards} = this.state
|
||||
const optionId = card.properties[activeView.groupById!]
|
||||
const optionId = dstCard.properties[activeView.groupById!]
|
||||
|
||||
if (draggedCards.length < 1 || draggedCards.includes(card)) {
|
||||
return
|
||||
}
|
||||
const draggedCardIds = Array.from(new Set(selectedCardIds).add(srcCard.id))
|
||||
|
||||
const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card'
|
||||
const description = draggedCardIds.length > 1 ? `drag ${draggedCardIds.length} cards` : 'drag card'
|
||||
|
||||
// Update card order
|
||||
let cardOrder = boardTree.orderedCards().map((o) => o.id)
|
||||
const draggedCardIds = draggedCards.map((o) => o.id)
|
||||
// Update dstCard order
|
||||
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])
|
||||
let cardOrder = orderedCards.map((o) => o.id)
|
||||
const firstDraggedCard = draggedCards[0]
|
||||
const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(card.id)
|
||||
const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(dstCard.id)
|
||||
cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
|
||||
let destIndex = cardOrder.indexOf(card.id)
|
||||
let destIndex = cardOrder.indexOf(dstCard.id)
|
||||
if (firstDraggedCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) {
|
||||
// If the cards are in the same column and dragging down, drop after the target card
|
||||
// If the cards are in the same column and dragging down, drop after the target dstCard
|
||||
destIndex += 1
|
||||
}
|
||||
cardOrder.splice(destIndex, 0, ...draggedCardIds)
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
import React, {useRef} from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
import {useDrag, useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
@ -20,60 +21,47 @@ type Props = {
|
||||
card: Card
|
||||
visiblePropertyTemplates: IPropertyTemplate[]
|
||||
isSelected: boolean
|
||||
isDropZone?: boolean
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||
onDragStart: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDragEnd: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
onDrop: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
onDrop: (srcCard: Card, dstCard: Card) => void
|
||||
}
|
||||
|
||||
const KanbanCard = (props: Props) => {
|
||||
const [isDragged, setIsDragged] = useState(false)
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
|
||||
const KanbanCard = React.memo((props: Props) => {
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const {card, intl} = props
|
||||
const [{isDragging}, drag] = useDrag(() => ({
|
||||
type: 'card',
|
||||
item: card,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}))
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (item: Card) => {
|
||||
props.onDrop(item, card)
|
||||
},
|
||||
}))
|
||||
|
||||
const visiblePropertyTemplates = props.visiblePropertyTemplates || []
|
||||
let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard'
|
||||
if (props.isDropZone && isDragOver) {
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
drop(drag(cardRef))
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={props.readonly ? () => null : cardRef}
|
||||
className={className}
|
||||
draggable={!props.readonly}
|
||||
style={{opacity: isDragged ? 0.5 : 1}}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
onClick={props.onClick}
|
||||
onDragStart={(e) => {
|
||||
setIsDragged(true)
|
||||
props.onDragStart(e)
|
||||
}}
|
||||
onDragEnd={(e) => {
|
||||
setIsDragged(false)
|
||||
props.onDragEnd(e)
|
||||
}}
|
||||
|
||||
onDragOver={() => {
|
||||
if (!isDragOver) {
|
||||
setIsDragOver(true)
|
||||
}
|
||||
}}
|
||||
onDragEnter={() => {
|
||||
if (!isDragOver) {
|
||||
setIsDragOver(true)
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setIsDragOver(false)
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
setIsDragOver(false)
|
||||
if (props.isDropZone) {
|
||||
props.onDrop(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!props.readonly &&
|
||||
<MenuWrapper
|
||||
@ -115,6 +103,6 @@ const KanbanCard = (props: Props) => {
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
export default injectIntl(KanbanCard)
|
||||
|
@ -1,45 +1,35 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState} from 'react'
|
||||
import React from 'react'
|
||||
import {useDrop} from 'react-dnd'
|
||||
import {Card} from '../../blocks/card'
|
||||
|
||||
type Props = {
|
||||
onDrop: (e: React.DragEvent<HTMLDivElement>) => void
|
||||
isDropZone: boolean
|
||||
onDrop: (card: Card) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const KanbanColumn = React.memo((props: Props) => {
|
||||
const [isDragOver, setIsDragOver] = useState(false)
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver({shallow: true}),
|
||||
}),
|
||||
drop: (item: Card, monitor) => {
|
||||
if (monitor.isOver({shallow: true})) {
|
||||
props.onDrop(item)
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
let className = 'octo-board-column'
|
||||
if (props.isDropZone && isDragOver) {
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={drop}
|
||||
className={className}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault()
|
||||
if (!isDragOver) {
|
||||
setIsDragOver(true)
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
e.preventDefault()
|
||||
if (!isDragOver) {
|
||||
setIsDragOver(true)
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
e.preventDefault()
|
||||
setIsDragOver(false)
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
setIsDragOver(false)
|
||||
if (props.isDropZone) {
|
||||
props.onDrop(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
|
@ -1,11 +1,13 @@
|
||||
// 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 React, {useState, useEffect, useRef} from 'react'
|
||||
import {FormattedMessage, IntlShape} from 'react-intl'
|
||||
import {useDrop, useDrag} from 'react-dnd'
|
||||
|
||||
import {Constants} from '../../constants'
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import {Card} from '../../blocks/card'
|
||||
import mutator from '../../mutator'
|
||||
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
|
||||
import Button from '../../widgets/buttons/button'
|
||||
@ -26,8 +28,7 @@ type Props = {
|
||||
readonly: boolean
|
||||
addCard: (groupByOptionId?: string) => Promise<void>
|
||||
propertyNameChanged: (option: IPropertyOption, text: string) => Promise<void>
|
||||
onDropToColumn: (option: IPropertyOption) => void
|
||||
setDraggedHeaderOption: (draggedHeaderOption?: IPropertyOption) => void
|
||||
onDropToColumn: (srcOption: IPropertyOption, card?: Card, dstOption?: IPropertyOption) => void
|
||||
}
|
||||
|
||||
export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
@ -35,42 +36,42 @@ export default function KanbanColumnHeader(props: Props): JSX.Element {
|
||||
const {activeView} = boardTree
|
||||
const [groupTitle, setGroupTitle] = useState(group.option.value)
|
||||
|
||||
const headerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [{isDragging}, drag] = useDrag(() => ({
|
||||
type: 'column',
|
||||
item: group.option,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}))
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'column',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (item: IPropertyOption) => {
|
||||
props.onDropToColumn(item, undefined, group.option)
|
||||
},
|
||||
}))
|
||||
|
||||
useEffect(() => {
|
||||
setGroupTitle(group.option.value)
|
||||
}, [group.option.value])
|
||||
|
||||
const ref = React.createRef<HTMLDivElement>()
|
||||
drop(drag(headerRef))
|
||||
let className = 'octo-board-header-cell KanbanColumnHeader'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.option.id || 'empty'}
|
||||
ref={ref}
|
||||
className='octo-board-header-cell KanbanColumnHeader'
|
||||
|
||||
ref={headerRef}
|
||||
style={{opacity: isDragging ? 0.5 : 1}}
|
||||
className={className}
|
||||
draggable={!props.readonly}
|
||||
onDragStart={() => {
|
||||
props.setDraggedHeaderOption(group.option)
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
props.setDraggedHeaderOption(undefined)
|
||||
}}
|
||||
|
||||
onDragOver={(e) => {
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
ref.current?.classList.add('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
ref.current?.classList.remove('dragover')
|
||||
e.preventDefault()
|
||||
props.onDropToColumn(group.option)
|
||||
}}
|
||||
>
|
||||
{!group.option.id &&
|
||||
<Label
|
||||
|
@ -3,6 +3,7 @@
|
||||
/* eslint-disable max-lines */
|
||||
import React, {useRef, useState} from 'react'
|
||||
import {IntlShape} from 'react-intl'
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyOption} from '../../blocks/board'
|
||||
import mutator from '../../mutator'
|
||||
@ -12,53 +13,39 @@ import Menu from '../../widgets/menu'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import ShowIcon from '../../widgets/icons/show'
|
||||
import Label from '../../widgets/label'
|
||||
import {Card} from '../../blocks/card'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
group: BoardTreeGroup
|
||||
intl: IntlShape
|
||||
readonly: boolean
|
||||
onDropToColumn: (option: IPropertyOption) => void
|
||||
hasDraggedCards: boolean
|
||||
onDrop: (card: Card) => void
|
||||
}
|
||||
|
||||
export default function KanbanHiddenColumnItem(props: Props): JSX.Element {
|
||||
const {boardTree, intl, group} = props
|
||||
const {activeView} = boardTree
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'card',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (item: Card) => {
|
||||
props.onDrop(item)
|
||||
},
|
||||
}))
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
const [dragClass, setDragClass] = useState('')
|
||||
let className = 'octo-board-hidden-item'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
ref={drop}
|
||||
key={group.option.id || 'empty'}
|
||||
className={`octo-board-hidden-item ${dragClass}`}
|
||||
onDragOver={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('dragover')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDragEnter={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('dragover')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDragLeave={(e) => {
|
||||
if (props.hasDraggedCards) {
|
||||
setDragClass('')
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onDrop={(e) => {
|
||||
setDragClass('')
|
||||
e.preventDefault()
|
||||
if (props.hasDraggedCards) {
|
||||
props.onDropToColumn(group.option)
|
||||
}
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<MenuWrapper
|
||||
disabled={props.readonly}
|
||||
|
@ -1,57 +1,26 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import {useDrag} from 'react-dnd'
|
||||
|
||||
import './horizontalGrip.scss'
|
||||
|
||||
type Props = {
|
||||
onDrag: (offset: number) => void
|
||||
onDragEnd: (offset: number) => void
|
||||
templateId: string
|
||||
}
|
||||
|
||||
type State = {
|
||||
isDragging: boolean
|
||||
startX: number
|
||||
offset: number
|
||||
}
|
||||
const HorizontalGrip = React.memo((props: Props): JSX.Element => {
|
||||
const [, drag] = useDrag(() => ({
|
||||
type: 'horizontalGrip',
|
||||
item: {id: props.templateId},
|
||||
}))
|
||||
|
||||
class HorizontalGrip extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isDragging: false,
|
||||
startX: 0,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className='HorizontalGrip'
|
||||
onMouseDown={(e) => {
|
||||
this.setState({isDragging: true, startX: e.clientX, offset: 0})
|
||||
window.addEventListener('mousemove', this.globalMouseMove)
|
||||
window.addEventListener('mouseup', this.globalMouseUp)
|
||||
}}
|
||||
/>)
|
||||
}
|
||||
|
||||
private globalMouseMove = (e: MouseEvent) => {
|
||||
if (!this.state.isDragging) {
|
||||
return
|
||||
}
|
||||
const offset = e.clientX - this.state.startX
|
||||
if (offset !== this.state.offset) {
|
||||
this.props.onDrag(offset)
|
||||
this.setState({offset})
|
||||
}
|
||||
}
|
||||
|
||||
private globalMouseUp = (e: MouseEvent) => {
|
||||
window.removeEventListener('mousemove', this.globalMouseMove)
|
||||
window.removeEventListener('mouseup', this.globalMouseUp)
|
||||
this.setState({isDragging: false})
|
||||
const offset = e.clientX - this.state.startX
|
||||
this.props.onDragEnd(offset)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
ref={drag}
|
||||
className='HorizontalGrip'
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
export default HorizontalGrip
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import React, {useState} from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
import {useDrop} from 'react-dnd'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {MutableBoardView} from '../../blocks/boardView'
|
||||
@ -10,15 +11,9 @@ import {Constants} from '../../constants'
|
||||
import mutator from '../../mutator'
|
||||
import {Utils} from '../../utils'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import SortDownIcon from '../../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import Label from '../../widgets/label'
|
||||
|
||||
import HorizontalGrip from './horizontalGrip'
|
||||
|
||||
import './table.scss'
|
||||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
import TableHeader from './tableHeader'
|
||||
import TableRow from './tableRow'
|
||||
|
||||
type Props = {
|
||||
@ -31,252 +26,143 @@ type Props = {
|
||||
onCardClicked: (e: React.MouseEvent, card: Card) => void
|
||||
}
|
||||
|
||||
type State = {
|
||||
shownCardId?: string
|
||||
}
|
||||
const Table = (props: Props) => {
|
||||
const {boardTree} = props
|
||||
const {board, cards, activeView} = boardTree
|
||||
const [changed, triggerChanged] = useState<number>(0)
|
||||
|
||||
class Table extends React.Component<Props, State> {
|
||||
private draggedHeaderTemplate?: IPropertyTemplate
|
||||
state: State = {}
|
||||
const [{offset, resizingColumn}, drop] = useDrop(() => ({
|
||||
accept: 'horizontalGrip',
|
||||
collect: (monitor) => ({
|
||||
offset: monitor.getDifferenceFromInitialOffset()?.x || 0,
|
||||
resizingColumn: monitor.getItem<{id: string}>()?.id,
|
||||
}),
|
||||
drop: (item: {id: string}, monitor) => {
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
const finalOffset = monitor.getDifferenceFromInitialOffset()?.x || 0
|
||||
const newWidth = Math.max(Constants.minColumnWidth, (columnWidths[item.id] || 0) + (finalOffset || 0))
|
||||
if (newWidth !== columnWidths[item.id]) {
|
||||
columnWidths[item.id] = newWidth
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return true
|
||||
}
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
},
|
||||
hover: () => {
|
||||
triggerChanged(changed + 1)
|
||||
},
|
||||
}), [changed])
|
||||
|
||||
render(): JSX.Element {
|
||||
const {boardTree} = this.props
|
||||
const {board, cards, activeView} = boardTree
|
||||
const titleRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
let titleSortIcon: React.ReactNode
|
||||
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
if (titleSortOption) {
|
||||
titleSortIcon = titleSortOption.reversed ? <SortUpIcon/> : <SortDownIcon/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='octo-table-body Table'>
|
||||
|
||||
{/* Headers */}
|
||||
|
||||
<div
|
||||
className='octo-table-header'
|
||||
id='mainBoardHeader'
|
||||
>
|
||||
<div
|
||||
id='mainBoardHeader'
|
||||
ref={titleRef}
|
||||
className='octo-table-cell header-cell'
|
||||
style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}}
|
||||
>
|
||||
<MenuWrapper disabled={this.props.readonly}>
|
||||
<Label>
|
||||
<FormattedMessage
|
||||
id='TableComponent.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
{titleSortIcon}
|
||||
</Label>
|
||||
<TableHeaderMenu
|
||||
boardTree={boardTree}
|
||||
templateId={Constants.titleColumnId}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!this.props.readonly &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (titleRef.current) {
|
||||
titleRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[Constants.titleColumnId]) {
|
||||
columnWidths[Constants.titleColumnId] = newWidth
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
let sortIcon
|
||||
const sortOption = activeView.sortOptions.find((o) => o.propertyId === template.id)
|
||||
if (sortOption) {
|
||||
sortIcon = sortOption.reversed ? <SortUpIcon/> : <SortDownIcon/>
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={template.id}
|
||||
ref={headerRef}
|
||||
style={{overflow: 'unset', width: this.columnWidth(template.id)}}
|
||||
className='octo-table-cell header-cell'
|
||||
|
||||
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)
|
||||
}}
|
||||
>
|
||||
<MenuWrapper
|
||||
disabled={this.props.readonly}
|
||||
>
|
||||
<div
|
||||
draggable={!this.props.readonly}
|
||||
onDragStart={() => {
|
||||
this.draggedHeaderTemplate = template
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.draggedHeaderTemplate = undefined
|
||||
}}
|
||||
>
|
||||
<Label>
|
||||
{template.name}
|
||||
{sortIcon}
|
||||
</Label>
|
||||
</div>
|
||||
<TableHeaderMenu
|
||||
boardTree={boardTree}
|
||||
templateId={template.id}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!this.props.readonly &&
|
||||
<HorizontalGrip
|
||||
onDrag={(offset) => {
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
}}
|
||||
onDragEnd={(offset) => {
|
||||
Utils.log(`onDragEnd offset: ${offset}`)
|
||||
const originalWidth = this.columnWidth(template.id)
|
||||
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||
if (headerRef.current) {
|
||||
headerRef.current.style.width = `${newWidth}px`
|
||||
}
|
||||
|
||||
const columnWidths = {...activeView.columnWidths}
|
||||
if (newWidth !== columnWidths[template.id]) {
|
||||
columnWidths[template.id] = newWidth
|
||||
|
||||
const newView = new MutableBoardView(activeView)
|
||||
newView.columnWidths = columnWidths
|
||||
mutator.updateBlock(newView, activeView, 'resize column')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rows, one per card */}
|
||||
|
||||
{cards.map((card) => {
|
||||
const tableRow = (
|
||||
<TableRow
|
||||
key={card.id + card.updateAt}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
isSelected={this.props.selectedCardIds.includes(card.id)}
|
||||
focusOnMount={this.props.cardIdToFocusOnRender === card.id}
|
||||
onSaveWithEnter={() => {
|
||||
if (cards.length > 0 && cards[cards.length - 1] === card) {
|
||||
this.props.addCard(false)
|
||||
}
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.props.onCardClicked(e, card)
|
||||
}}
|
||||
showCard={this.props.showCard}
|
||||
readonly={this.props.readonly}
|
||||
/>)
|
||||
|
||||
return tableRow
|
||||
})}
|
||||
|
||||
{/* Add New row */}
|
||||
|
||||
<div className='octo-table-footer'>
|
||||
{!this.props.readonly &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
this.props.addCard(false)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private columnWidth(templateId: string): number {
|
||||
return Math.max(Constants.minColumnWidth, this.props.boardTree.activeView.columnWidths[templateId] || 0)
|
||||
}
|
||||
|
||||
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}`)
|
||||
const onDropToColumn = async (template: IPropertyTemplate, container: IPropertyTemplate) => {
|
||||
Utils.log(`ondrop. Source column: ${template.name}, dest column: ${container.name}`)
|
||||
|
||||
// Move template to new index
|
||||
const destIndex = template ? board.cardProperties.indexOf(template) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex)
|
||||
const destIndex = container ? board.cardProperties.indexOf(container) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, template, destIndex >= 0 ? destIndex : 0)
|
||||
}
|
||||
|
||||
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
|
||||
let titleSorted: 'up' | 'down' | 'none' = 'none'
|
||||
if (titleSortOption) {
|
||||
titleSorted = titleSortOption.reversed ? 'up' : 'down'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='octo-table-body Table'>
|
||||
|
||||
{/* Headers */}
|
||||
|
||||
<div
|
||||
className='octo-table-header'
|
||||
id='mainBoardHeader'
|
||||
ref={drop}
|
||||
>
|
||||
<TableHeader
|
||||
name={
|
||||
<FormattedMessage
|
||||
id='TableComponent.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
}
|
||||
sorted={titleSorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
template={{id: Constants.titleColumnId, name: 'title', type: 'text', options: []}}
|
||||
offset={resizingColumn === Constants.titleColumnId ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
/>
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) => {
|
||||
let sorted: 'up' | 'down' | 'none' = 'none'
|
||||
const sortOption = activeView.sortOptions.find((o) => o.propertyId === template.id)
|
||||
if (sortOption) {
|
||||
sorted = sortOption.reversed ? 'up' : 'down'
|
||||
}
|
||||
|
||||
return (
|
||||
<TableHeader
|
||||
name={template.name}
|
||||
sorted={sorted}
|
||||
readonly={props.readonly}
|
||||
boardTree={boardTree}
|
||||
template={template}
|
||||
key={template.id}
|
||||
offset={resizingColumn === template.id ? offset : 0}
|
||||
onDrop={onDropToColumn}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Rows, one per card */}
|
||||
|
||||
{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(false)
|
||||
}
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLDivElement>) => {
|
||||
props.onCardClicked(e, card)
|
||||
}}
|
||||
showCard={props.showCard}
|
||||
readonly={props.readonly}
|
||||
/>)
|
||||
|
||||
return tableRow
|
||||
})}
|
||||
|
||||
{/* Add New row */}
|
||||
|
||||
<div className='octo-table-footer'>
|
||||
{!props.readonly &&
|
||||
<div
|
||||
className='octo-table-cell'
|
||||
onClick={() => {
|
||||
props.addCard(false)
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='TableComponent.plus-new'
|
||||
defaultMessage='+ New'
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Table
|
||||
|
90
webapp/src/components/table/tableHeader.tsx
Normal file
90
webapp/src/components/table/tableHeader.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React, {useState, useRef} from 'react'
|
||||
import {useDrop, useDrag} from 'react-dnd'
|
||||
|
||||
import {IPropertyTemplate} from '../../blocks/board'
|
||||
import {Constants} from '../../constants'
|
||||
import {BoardTree} from '../../viewModel/boardTree'
|
||||
import SortDownIcon from '../../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../../widgets/icons/sortUp'
|
||||
import MenuWrapper from '../../widgets/menuWrapper'
|
||||
import Label from '../../widgets/label'
|
||||
|
||||
import HorizontalGrip from './horizontalGrip'
|
||||
|
||||
import './table.scss'
|
||||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
|
||||
type Props = {
|
||||
readonly: boolean
|
||||
sorted: 'up'|'down'|'none'
|
||||
name: React.ReactNode
|
||||
boardTree: BoardTree
|
||||
template: IPropertyTemplate
|
||||
offset: number
|
||||
onDrop?: (template: IPropertyTemplate, container: IPropertyTemplate) => void
|
||||
}
|
||||
|
||||
const TableHeader = React.memo((props: Props): JSX.Element => {
|
||||
const columnRef = useRef<HTMLDivElement>(null)
|
||||
const [{isDragging}, drag] = useDrag(() => ({
|
||||
type: 'column',
|
||||
item: props.template,
|
||||
collect: (monitor) => ({
|
||||
isDragging: monitor.isDragging(),
|
||||
}),
|
||||
}))
|
||||
const [{isOver}, drop] = useDrop(() => ({
|
||||
accept: 'column',
|
||||
collect: (monitor) => ({
|
||||
isOver: monitor.isOver(),
|
||||
}),
|
||||
drop: (item: IPropertyTemplate) => {
|
||||
props.onDrop && props.onDrop(item, props.template)
|
||||
},
|
||||
}))
|
||||
|
||||
const columnWidth = (templateId: string): number => {
|
||||
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
|
||||
}
|
||||
|
||||
if (props.template.id === Constants.titleColumnId) {
|
||||
drop(columnRef)
|
||||
} else {
|
||||
drop(drag(columnRef))
|
||||
}
|
||||
|
||||
let className = 'octo-table-cell header-cell'
|
||||
if (isOver) {
|
||||
className += ' dragover'
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={{overflow: 'unset', width: columnWidth(props.template.id), opacity: isDragging ? 0.5 : 1}}
|
||||
ref={props.onDrop ? columnRef : () => null}
|
||||
>
|
||||
<MenuWrapper disabled={props.readonly}>
|
||||
<Label>
|
||||
{props.name}
|
||||
{props.sorted === 'up' && <SortUpIcon/>}
|
||||
{props.sorted === 'down' && <SortDownIcon/>}
|
||||
</Label>
|
||||
<TableHeaderMenu
|
||||
boardTree={props.boardTree}
|
||||
templateId={props.template.id}
|
||||
/>
|
||||
</MenuWrapper>
|
||||
|
||||
<div className='octo-spacer'/>
|
||||
|
||||
{!props.readonly &&
|
||||
<HorizontalGrip templateId={props.template.id}/>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default TableHeader
|
Loading…
x
Reference in New Issue
Block a user