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:
commit
7320f1e7a1
66
webapp/package-lock.json
generated
66
webapp/package-lock.json
generated
@ -1646,6 +1646,21 @@
|
||||
"fastq": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"@react-dnd/asap": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.0.tgz",
|
||||
"integrity": "sha512-0XhqJSc6pPoNnf8DhdsPHtUhRzZALVzYMTzRwV4VI6DJNJ/5xxfL9OQUwb8IH5/2x7lSf7nAZrnzUD+16VyOVQ=="
|
||||
},
|
||||
"@react-dnd/invariant": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz",
|
||||
"integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw=="
|
||||
},
|
||||
"@react-dnd/shallowequal": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz",
|
||||
"integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg=="
|
||||
},
|
||||
"@samverschueren/stream-to-observable": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.1.tgz",
|
||||
@ -4331,6 +4346,16 @@
|
||||
"path-type": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"dnd-core": {
|
||||
"version": "14.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-14.0.0.tgz",
|
||||
"integrity": "sha512-wTDYKyjSqWuYw3ZG0GJ7k+UIfzxTNoZLjDrut37PbcPGNfwhlKYlPUqjAKUjOOv80izshUiqusaKgJPItXSevA==",
|
||||
"requires": {
|
||||
"@react-dnd/asap": "^4.0.0",
|
||||
"@react-dnd/invariant": "^2.0.0",
|
||||
"redux": "^4.0.5"
|
||||
}
|
||||
},
|
||||
"doctrine": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
|
||||
@ -10185,6 +10210,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",
|
||||
|
@ -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",
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -13,4 +13,7 @@
|
||||
> .octo-block-margin {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.ImageElement {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -56,6 +56,7 @@
|
||||
overflow: hidden;
|
||||
max-height: 160px;
|
||||
min-height: 160px;
|
||||
pointer-events: none;
|
||||
|
||||
.ImageElement {
|
||||
width: 100%;
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
69
webapp/src/components/table/tableHeader.tsx
Normal file
69
webapp/src/components/table/tableHeader.tsx
Normal 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
|
@ -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 */}
|
||||
|
29
webapp/src/hooks/sortable.tsx
Normal file
29
webapp/src/hooks/sortable.tsx
Normal 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]
|
||||
}
|
@ -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}
|
||||
|
@ -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%;
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user