From 13dbecc8234d99d81dd919c7450c7ddb7938c717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 7 Apr 2021 15:51:19 +0200 Subject: [PATCH] Migrating all drag and drop into react-dnd --- webapp/package-lock.json | 57 ++- webapp/package.json | 2 + webapp/src/app.tsx | 142 +++---- webapp/src/components/kanban/kanban.tsx | 81 ++-- webapp/src/components/kanban/kanbanCard.tsx | 68 ++-- webapp/src/components/kanban/kanbanColumn.tsx | 44 +- .../components/kanban/kanbanColumnHeader.tsx | 63 +-- .../kanban/kanbanHiddenColumnItem.tsx | 49 +-- .../src/components/table/horizontalGrip.tsx | 59 +-- webapp/src/components/table/table.tsx | 382 ++++++------------ webapp/src/components/table/tableHeader.tsx | 90 +++++ 11 files changed, 500 insertions(+), 537 deletions(-) create mode 100644 webapp/src/components/table/tableHeader.tsx diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 524ab6fca..ed9aca251 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -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", diff --git a/webapp/package.json b/webapp/package.json index f5489d348..710c1a131 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -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", diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 530a6d80c..ad17a9190 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -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 { locale={this.state.language} messages={getMessages(this.state.language)} > - - - -
-
- - - - - - - - - - - - - - - + + + +
+
+ + + + + + + + + + + + + + + + + + {this.state.initialLoad && !this.state.user && } + + + { + return ( + + ) + }} /> - - - {this.state.initialLoad && !this.state.user && } - { + if (this.state.initialLoad && !this.state.user) { + const redirectUrl = `/workspace/${match.params.workspaceId}/` + const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}` + return + } + return ( + + ) + }} /> - - { - return ( - - ) - }} - /> - { - if (this.state.initialLoad && !this.state.user) { - const redirectUrl = `/workspace/${match.params.workspaceId}/` - const loginUrl = `/login?r=${encodeURIComponent(redirectUrl)}` - return - } - return ( - - ) - }} - /> - - {this.state.initialLoad && !this.state.user && } - - - + + {this.state.initialLoad && !this.state.user && } + + + +
-
- - + + + ) } diff --git a/webapp/src/components/kanban/kanban.tsx b/webapp/src/components/kanban/kanban.tsx index ba4a01a6a..b1b6bcab1 100644 --- a/webapp/src/components/kanban/kanban.tsx +++ b/webapp/src/components/kanban/kanban.tsx @@ -78,9 +78,6 @@ class Kanban extends React.Component { 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 { {visibleGroups.map((group) => ( this.onDropToColumn(group.option)} + onDrop={(card: Card) => this.onDropToColumn(group.option, card)} > {group.cards.map((card) => ( { 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 { 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)} /> ))}
} @@ -206,14 +187,24 @@ class Kanban extends React.Component { 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 { } 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 { } } - 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) diff --git a/webapp/src/components/kanban/kanbanCard.tsx b/webapp/src/components/kanban/kanbanCard.tsx index 60e95d797..ea51837e8 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -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) => void - onDragStart: (e: React.DragEvent) => void - onDragEnd: (e: React.DragEvent) => void - onDrop: (e: React.DragEvent) => 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(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 (
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 && { ))}
) -} +}) export default injectIntl(KanbanCard) diff --git a/webapp/src/components/kanban/kanbanColumn.tsx b/webapp/src/components/kanban/kanbanColumn.tsx index a6ba7c311..448ef097e 100644 --- a/webapp/src/components/kanban/kanbanColumn.tsx +++ b/webapp/src/components/kanban/kanbanColumn.tsx @@ -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) => 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 (
{ - 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}
diff --git a/webapp/src/components/kanban/kanbanColumnHeader.tsx b/webapp/src/components/kanban/kanbanColumnHeader.tsx index 11354434b..bb8841ba8 100644 --- a/webapp/src/components/kanban/kanbanColumnHeader.tsx +++ b/webapp/src/components/kanban/kanbanColumnHeader.tsx @@ -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 propertyNameChanged: (option: IPropertyOption, text: string) => Promise - 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(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() + drop(drag(headerRef)) + let className = 'octo-board-header-cell KanbanColumnHeader' + if (isOver) { + className += ' dragover' + } + return (
{ - 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 &&