diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 524ab6fca..94ea6900e 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,35 @@ "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-dnd-touch-backend": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-14.0.0.tgz", + "integrity": "sha512-fNt3isf9h0xgjj86dIXhBi3dJ7OhC88vKUYdEvsOGypdp3LOFD+TobBAuBu00v26WmJ6II6xqbkhOx+KOcyHxQ==", + "requires": { + "@react-dnd/invariant": "^2.0.0", + "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 +10482,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 +11740,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..b69062e60 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -25,6 +25,9 @@ "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-dnd-touch-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..aa6dba471 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -8,6 +8,9 @@ import { Route, Switch, } from 'react-router-dom' +import {DndProvider} from 'react-dnd' +import {HTML5Backend} from 'react-dnd-html5-backend' +import {TouchBackend} from 'react-dnd-touch-backend' import {FlashMessages} from './components/flashMessages' import {getCurrentLanguage, getMessages, storeLanguage} from './i18n' @@ -18,6 +21,7 @@ import ErrorPage from './pages/errorPage' import LoginPage from './pages/loginPage' import RegisterPage from './pages/registerPage' import {IUser, UserContext} from './user' +import {Utils} from './utils' type State = { language: string, @@ -51,78 +55,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/cardDetail/cardDetailContents.tsx b/webapp/src/components/cardDetail/cardDetailContents.tsx index 9a5032151..db82411ac 100644 --- a/webapp/src/components/cardDetail/cardDetailContents.tsx +++ b/webapp/src/components/cardDetail/cardDetailContents.tsx @@ -3,6 +3,7 @@ import React from 'react' import {injectIntl, IntlShape} from 'react-intl' +import {IContentBlock} from '../../blocks/contentBlock' import {MutableTextBlock} from '../../blocks/textBlock' import mutator from '../../mutator' import {CardTree} from '../../viewModel/cardTree' @@ -32,6 +33,22 @@ function addTextBlock(card: Card, intl: IntlShape, text: string): void { }) } +function moveBlock(card: Card, srcBlock: IContentBlock, dstBlock: IContentBlock, intl: IntlShape): void { + let contentOrder = card.contentOrder.slice() + const isDraggingDown = contentOrder.indexOf(srcBlock.id) <= contentOrder.indexOf(dstBlock.id) + contentOrder = contentOrder.filter((id) => srcBlock.id !== id) + let destIndex = contentOrder.indexOf(dstBlock.id) + if (isDraggingDown) { + destIndex += 1 + } + contentOrder.splice(destIndex, 0, srcBlock.id) + + mutator.performAsUndoGroup(async () => { + const description = intl.formatMessage({id: 'CardDetail.moveContent', defaultMessage: 'move card content'}) + await mutator.changeCardContentOrder(card, contentOrder, description) + }) +} + const CardDetailContents = React.memo((props: Props) => { const {cardTree} = props if (!cardTree) { @@ -49,6 +66,7 @@ const CardDetailContents = React.memo((props: Props) => { card={card} contents={cardTree.contents} readonly={props.readonly} + onDrop={(src, dst) => moveBlock(card, src, dst, props.intl)} /> ))}
diff --git a/webapp/src/components/contentBlock.scss b/webapp/src/components/contentBlock.scss index 2933c1cf5..8ada56d1f 100644 --- a/webapp/src/components/contentBlock.scss +++ b/webapp/src/components/contentBlock.scss @@ -13,4 +13,7 @@ > .octo-block-margin { flex: 0 0 auto; } + .ImageElement { + pointer-events: none; + } } diff --git a/webapp/src/components/contentBlock.tsx b/webapp/src/components/contentBlock.tsx index 73fc1e331..6cd782891 100644 --- a/webapp/src/components/contentBlock.tsx +++ b/webapp/src/components/contentBlock.tsx @@ -16,6 +16,7 @@ import SortDownIcon from '../widgets/icons/sortDown' import SortUpIcon from '../widgets/icons/sortUp' import Menu from '../widgets/menu' import MenuWrapper from '../widgets/menuWrapper' +import useSortable from '../hooks/sortable' import ContentElement from './content/contentElement' import AddContentMenuItem from './addContentMenuItem' @@ -28,14 +29,24 @@ type Props = { contents: readonly IContentBlock[] readonly: boolean intl: IntlShape + onDrop: (srctBlock: IContentBlock, dstBlock: IContentBlock) => void } const ContentBlock = React.memo((props: Props): JSX.Element => { const {intl, card, contents, block, readonly} = props + const [isDragging, isOver, contentRef] = useSortable('content', block, true, props.onDrop) const index = contents.indexOf(block) + let className = 'ContentBlock octo-block' + if (isOver) { + className += ' dragover' + } return ( -
+
{!props.readonly && diff --git a/webapp/src/components/gallery/gallery.scss b/webapp/src/components/gallery/gallery.scss index 7ba76a29f..4234ab48e 100644 --- a/webapp/src/components/gallery/gallery.scss +++ b/webapp/src/components/gallery/gallery.scss @@ -8,7 +8,7 @@ color: rgba(var(--body-color), 0.3); cursor: pointer; width: 280px; - min-height: 200px; + min-height: 160px; display: flex; flex-direction: column; align-items: center; diff --git a/webapp/src/components/gallery/gallery.tsx b/webapp/src/components/gallery/gallery.tsx index 212994c80..abd026c22 100644 --- a/webapp/src/components/gallery/gallery.tsx +++ b/webapp/src/components/gallery/gallery.tsx @@ -5,6 +5,8 @@ import {FormattedMessage} from 'react-intl' import {Constants} from '../../constants' import {Card} from '../../blocks/card' +import mutator from '../../mutator' +import {Utils} from '../../utils' import {BoardTree} from '../../viewModel/boardTree' import {CardTree, MutableCardTree} from '../../viewModel/cardTree' import useCardListener from '../../hooks/cardListener' @@ -22,9 +24,33 @@ type Props = { const Gallery = (props: Props): JSX.Element => { const {boardTree} = props - const {cards} = boardTree + const {cards, activeView} = boardTree const visiblePropertyTemplates = boardTree.board.cardProperties.filter((template) => boardTree.activeView.visiblePropertyIds.includes(template.id)) const [cardTrees, setCardTrees] = useState<{[key: string]: CardTree | undefined}>({}) + const isManualSort = activeView.sortOptions.length < 1 + + 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 + } + cardOrder.splice(destIndex, 0, ...draggedCardIds) + + mutator.performAsUndoGroup(async () => { + await mutator.changeViewCardOrder(activeView, cardOrder, description) + }) + } + const visibleTitle = boardTree.activeView.visiblePropertyIds.includes(Constants.titleColumnId) useCardListener( @@ -65,6 +91,8 @@ const Gallery = (props: Props): JSX.Element => { visibleTitle={visibleTitle} isSelected={props.selectedCardIds.includes(card.id)} readonly={props.readonly} + onDrop={onDropToCard} + isManualSort={isManualSort} /> ) } diff --git a/webapp/src/components/gallery/galleryCard.scss b/webapp/src/components/gallery/galleryCard.scss index 85ab33e14..cc58dcb45 100644 --- a/webapp/src/components/gallery/galleryCard.scss +++ b/webapp/src/components/gallery/galleryCard.scss @@ -56,6 +56,7 @@ overflow: hidden; max-height: 160px; min-height: 160px; + pointer-events: none; .ImageElement { width: 100%; diff --git a/webapp/src/components/gallery/galleryCard.tsx b/webapp/src/components/gallery/galleryCard.tsx index 915722df9..51bc1186f 100644 --- a/webapp/src/components/gallery/galleryCard.tsx +++ b/webapp/src/components/gallery/galleryCard.tsx @@ -3,7 +3,6 @@ import React from 'react' import {FormattedMessage, injectIntl, IntlShape} from 'react-intl' -import {Constants} from '../../constants' import {IPropertyTemplate} from '../../blocks/board' import {Card} from '../../blocks/card' import {CardTree} from '../../viewModel/cardTree' @@ -16,6 +15,7 @@ import DuplicateIcon from '../../widgets/icons/duplicate' import OptionsIcon from '../../widgets/icons/options' import Menu from '../../widgets/menu' import MenuWrapper from '../../widgets/menuWrapper' +import useSortable from '../../hooks/sortable' import ImageElement from '../content/imageElement' import ContentElement from '../content/contentElement' @@ -31,20 +31,29 @@ type Props = { isSelected: boolean intl: IntlShape readonly: boolean + isManualSort: boolean + onDrop: (srcCard: Card, dstCard: Card) => void } const GalleryCard = React.memo((props: Props) => { const {cardTree} = props + const [isDragging, isOver, cardRef] = useSortable('card', cardTree.card, props.isManualSort, props.onDrop) const visiblePropertyTemplates = props.visiblePropertyTemplates || [] let images: IContentBlock[] = [] images = cardTree.contents.filter((content) => content.type === 'image') + let className = props.isSelected ? 'GalleryCard selected' : 'GalleryCard' + if (isOver) { + className += ' dragover' + } return (
props.onClick(e, cardTree.card)} + style={{opacity: isDragging ? 0.5 : 1}} + ref={cardRef} > {!props.readonly && { 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} + isManualSort={isManualSort} /> ))} {!this.props.readonly && @@ -176,8 +159,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 +188,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 +218,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 +234,29 @@ 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) - const firstDraggedCard = draggedCards[0] - const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(card.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 isDraggingDown = cardOrder.indexOf(srcCard.id) <= cardOrder.indexOf(dstCard.id) cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id)) - let destIndex = cardOrder.indexOf(card.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 + let destIndex = cardOrder.indexOf(dstCard.id) + if (srcCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) { + // 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..bc8f02df4 100644 --- a/webapp/src/components/kanban/kanbanCard.tsx +++ b/webapp/src/components/kanban/kanbanCard.tsx @@ -1,6 +1,6 @@ // 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 {injectIntl, IntlShape} from 'react-intl' import {IPropertyTemplate} from '../../blocks/board' @@ -12,6 +12,7 @@ import DuplicateIcon from '../../widgets/icons/duplicate' import OptionsIcon from '../../widgets/icons/options' import Menu from '../../widgets/menu' import MenuWrapper from '../../widgets/menuWrapper' +import useSortable from '../../hooks/sortable' import './kanbanCard.scss' import PropertyValueElement from '../propertyValueElement' @@ -20,60 +21,29 @@ 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 + isManualSort: boolean } -const KanbanCard = (props: Props) => { - const [isDragged, setIsDragged] = useState(false) - const [isDragOver, setIsDragOver] = useState(false) - +const KanbanCard = React.memo((props: Props) => { const {card, intl} = props + const [isDragging, isOver, cardRef] = useSortable('card', card, props.isManualSort, props.onDrop) const visiblePropertyTemplates = props.visiblePropertyTemplates || [] let className = props.isSelected ? 'KanbanCard selected' : 'KanbanCard' - if (props.isDropZone && isDragOver) { + if (isOver) { className += ' dragover' } 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..ffe4b88bc 100644 --- a/webapp/src/components/kanban/kanbanColumn.tsx +++ b/webapp/src/components/kanban/kanbanColumn.tsx @@ -1,45 +1,36 @@ // 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 &&