1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-17 18:26:17 +02:00

Merge pull request #238 from jespino/react-dnd

Migrating all drag and drop into react-dnd
This commit is contained in:
Jesús Espino 2021-04-08 20:19:28 +02:00 committed by GitHub
commit 7320f1e7a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 659 additions and 551 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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<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={Utils.isMobile() ? TouchBackend : 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>
)
}

View File

@ -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)}
/>
))}
</div>

View File

@ -13,4 +13,7 @@
> .octo-block-margin {
flex: 0 0 auto;
}
.ImageElement {
pointer-events: none;
}
}

View File

@ -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 (
<div className='ContentBlock octo-block'>
<div
className={className}
style={{opacity: isDragging ? 0.5 : 1}}
ref={contentRef}
>
<div className='octo-block-margin'>
{!props.readonly &&
<MenuWrapper>

View File

@ -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;

View File

@ -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}
/>
)
}

View File

@ -56,6 +56,7 @@
overflow: hidden;
max-height: 160px;
min-height: 160px;
pointer-events: none;
.ImageElement {
width: 100%;

View File

@ -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 (
<div
className={`GalleryCard ${props.isSelected ? 'selected' : ''}`}
className={className}
onClick={(e: React.MouseEvent) => props.onClick(e, cardTree.card)}
style={{opacity: isDragging ? 0.5 : 1}}
ref={cardRef}
>
{!props.readonly &&
<MenuWrapper

View File

@ -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,8 @@ 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}
isManualSort={isManualSort}
/>
))}
{!this.props.readonly &&
@ -176,8 +159,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 +188,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 +218,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 +234,29 @@ 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)
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)

View File

@ -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<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
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 (
<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 +85,6 @@ const KanbanCard = (props: Props) => {
))}
</div>
)
}
})
export default injectIntl(KanbanCard)

View File

@ -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<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>

View File

@ -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

View File

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React, {useRef, useState} from 'react'
import React from 'react'
import {IntlShape} from 'react-intl'
import {useDrop} from 'react-dnd'
import {IPropertyOption} from '../../blocks/board'
import mutator from '../../mutator'
import {BoardTree, BoardTreeGroup} from '../../viewModel/boardTree'
import Button from '../../widgets/buttons/button'
@ -12,53 +12,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}

View File

@ -103,7 +103,7 @@ const PropertyValueElement = (props:Props): JSX.Element => {
onChange={setValue}
onSave={() => mutator.changePropertyValue(card, propertyTemplate.id, value)}
onCancel={() => setValue(propertyValue)}
validator={(value) => validateProp(propertyTemplate.type, value)}
validator={(newValue) => validateProp(propertyTemplate.type, newValue)}
/>
)
}

View File

@ -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

View File

@ -2,6 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {useDrop, useDragLayer} 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,174 @@ 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
class Table extends React.Component<Props, State> {
private draggedHeaderTemplate?: IPropertyTemplate
state: State = {}
shouldComponentUpdate(): boolean {
return true
}
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/>
const {offset, resizingColumn} = useDragLayer((monitor) => {
if (monitor.getItemType() === 'horizontalGrip') {
return {
offset: monitor.getDifferenceFromInitialOffset()?.x || 0,
resizingColumn: monitor.getItem()?.id,
}
}
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
return {
offset: 0,
resizingColumn: '',
}
})
const {boardTree} = this.props
const {board} = boardTree
const [, drop] = useDrop(() => ({
accept: 'horizontalGrip',
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
Utils.assertValue(mutator)
Utils.assertValue(boardTree)
const newView = new MutableBoardView(activeView)
newView.columnWidths = columnWidths
mutator.updateBlock(newView, activeView, 'resize column')
}
},
}), [activeView])
Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`)
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 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'
ref={drop}
>
{/* Headers */}
<div
className='octo-table-header'
id='mainBoardHeader'
>
<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}
onDrop={onDropToCard}
offset={offset}
resizingColumn={resizingColumn}
/>)
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

View File

@ -0,0 +1,69 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
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 useSortable from '../../hooks/sortable'
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 isManualSort = props.boardTree.activeView.sortOptions.length < 1
const [isDragging, isOver, columnRef] = useSortable('column', props.template, isManualSort, props.onDrop)
const columnWidth = (templateId: string): number => {
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
}
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.template.id === Constants.titleColumnId ? () => null : columnRef}
>
<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

View File

@ -9,6 +9,7 @@ import mutator from '../../mutator'
import {BoardTree} from '../../viewModel/boardTree'
import Button from '../../widgets/buttons/button'
import Editable from '../../widgets/editable'
import useSortable from '../../hooks/sortable'
import PropertyValueElement from '../propertyValueElement'
import './tableRow.scss'
@ -21,12 +22,21 @@ type Props = {
onSaveWithEnter: () => void
showCard: (cardId: string) => void
readonly: boolean
offset: number
resizingColumn: string
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
onDrop: (srcCard: Card, dstCard: Card) => void
}
const TableRow = React.memo((props: Props) => {
const {boardTree, onSaveWithEnter} = props
const {board, activeView} = boardTree
const titleRef = useRef<Editable>(null)
const [title, setTitle] = useState(props.card.title)
const {card} = props
const isManualSort = activeView.sortOptions.length < 1
const [isDragging, isOver, cardRef] = useSortable('card', card, isManualSort, props.onDrop)
useEffect(() => {
if (props.focusOnMount) {
@ -35,18 +45,23 @@ const TableRow = React.memo((props: Props) => {
}, [])
const columnWidth = (templateId: string): number => {
if (props.resizingColumn === templateId) {
return Math.max(Constants.minColumnWidth, (props.boardTree.activeView.columnWidths[templateId] || 0) + props.offset)
}
return Math.max(Constants.minColumnWidth, props.boardTree.activeView.columnWidths[templateId] || 0)
}
const {boardTree, card, onSaveWithEnter} = props
const {board, activeView} = boardTree
const className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row'
let className = props.isSelected ? 'TableRow octo-table-row selected' : 'TableRow octo-table-row'
if (isOver) {
className += ' dragover'
}
return (
<div
className={className}
onClick={props.onClick}
ref={cardRef}
style={{opacity: isDragging ? 0.5 : 1}}
>
{/* Name / title */}

View File

@ -0,0 +1,29 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useRef} from 'react'
import {useDrag, useDrop} from 'react-dnd'
export default function useSortable(itemType: string, item: any, enabled: boolean, handler: (src: any, st: any) => void): [boolean, boolean, React.RefObject<HTMLDivElement>] {
const ref = useRef<HTMLDivElement>(null)
const [{isDragging}, drag] = useDrag(() => ({
type: itemType,
item,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
canDrag: () => enabled,
}), [itemType, item, enabled])
const [{isOver}, drop] = useDrop(() => ({
accept: itemType,
collect: (monitor) => ({
isOver: monitor.isOver(),
}),
drop: (dragItem: any) => {
handler(dragItem, item)
},
canDrop: () => enabled,
}), [item, handler, enabled])
drop(drag(ref))
return [isDragging, isOver, ref]
}

View File

@ -231,6 +231,22 @@ class Utils {
return result
}
static isMobile() {
const toMatch = [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i,
]
return toMatch.some((toMatchItem) => {
return navigator.userAgent.match(toMatchItem)
})
}
}
export {Utils}

View File

@ -2,18 +2,24 @@
width: 100%;
border-radius: var(--default-rad);
color: rgb(var(--body-color));
&:hover {
background-color: rgba(var(--body-color), 0.1),
}
display: flex;
> .Label {
margin: 0 10px;
max-width: calc(100% - 10px);
&.empty {
color: rgba(var(--body-color), 0.6);
}
}
.Label {
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
border-radius: var(--default-rad);
max-width: 100%;
}
.value-menu-option {
display: flex;
width: 100%;

View File

@ -107,14 +107,19 @@ function ValueSelector(props: Props): JSX.Element {
}),
control: (): CSSObject => ({
border: 0,
width: '100%',
margin: '4px 0 0 0',
}),
valueContainer: (provided: CSSObject): CSSObject => ({
...provided,
padding: '0 8px',
overflow: 'unset',
}),
singleValue: (provided: CSSObject): CSSObject => ({
...provided,
color: 'rgb(var(--main-fg))',
overflow: 'unset',
maxWidth: 'calc(100% - 20px)',
}),
input: (provided: CSSObject): CSSObject => ({
...provided,