mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-23 18:34:02 +02:00
Resize table columns
This commit is contained in:
parent
3ae9b2fc1f
commit
2fb38dda0a
@ -17,6 +17,7 @@ interface BoardView extends IBlock {
|
|||||||
readonly hiddenOptionIds: readonly string[]
|
readonly hiddenOptionIds: readonly string[]
|
||||||
readonly filter: FilterGroup | undefined
|
readonly filter: FilterGroup | undefined
|
||||||
readonly cardOrder: readonly string[]
|
readonly cardOrder: readonly string[]
|
||||||
|
readonly columnWidths: Readonly<Record<string, number>>
|
||||||
}
|
}
|
||||||
|
|
||||||
class MutableBoardView extends MutableBlock {
|
class MutableBoardView extends MutableBlock {
|
||||||
@ -76,6 +77,13 @@ class MutableBoardView extends MutableBlock {
|
|||||||
this.fields.cardOrder = value
|
this.fields.cardOrder = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get columnWidths(): Record<string, number> {
|
||||||
|
return this.fields.columnWidths as Record<string, number>
|
||||||
|
}
|
||||||
|
set columnWidths(value: Record<string, number>) {
|
||||||
|
this.fields.columnWidths = value
|
||||||
|
}
|
||||||
|
|
||||||
constructor(block: any = {}) {
|
constructor(block: any = {}) {
|
||||||
super(block)
|
super(block)
|
||||||
|
|
||||||
@ -87,6 +95,7 @@ class MutableBoardView extends MutableBlock {
|
|||||||
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
|
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
|
||||||
this.filter = new FilterGroup(block.fields?.filter)
|
this.filter = new FilterGroup(block.fields?.filter)
|
||||||
this.cardOrder = block.fields?.cardOrder?.slice() || []
|
this.cardOrder = block.fields?.cardOrder?.slice() || []
|
||||||
|
this.columnWidths = {...(block.fields?.columnWidths || {})}
|
||||||
|
|
||||||
if (!this.viewType) {
|
if (!this.viewType) {
|
||||||
this.viewType = 'board'
|
this.viewType = 'board'
|
||||||
|
8
webapp/src/components/horizontalGrip.scss
Normal file
8
webapp/src/components/horizontalGrip.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.HorizontalGrip {
|
||||||
|
width: 5px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(90, 192, 255, 0.7);
|
||||||
|
}
|
||||||
|
}
|
54
webapp/src/components/horizontalGrip.tsx
Normal file
54
webapp/src/components/horizontalGrip.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import './horizontalGrip.scss'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onDrag: (offset: number) => void
|
||||||
|
onDragEnd: (offset: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
isDragging?: boolean
|
||||||
|
startX?: number
|
||||||
|
offset?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
class HorizontalGrip extends React.PureComponent<Props, State> {
|
||||||
|
state: State = {
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {HorizontalGrip}
|
@ -9,7 +9,6 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 5px 8px 6px 8px;
|
padding: 5px 8px 6px 8px;
|
||||||
|
|
||||||
width: 150px;
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@ -18,13 +17,18 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.octo-icontitle {
|
.octo-icontitle {
|
||||||
|
flex: 1 1 auto;
|
||||||
.octo-icon {
|
.octo-icon {
|
||||||
min-width: 20px;
|
min-width: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Editable {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.title-cell {
|
&.header-cell {
|
||||||
width: 280px;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {FormattedMessage} from 'react-intl'
|
import {FormattedMessage} from 'react-intl'
|
||||||
|
|
||||||
|
import {Constants} from '../constants'
|
||||||
import {BlockIcons} from '../blockIcons'
|
import {BlockIcons} from '../blockIcons'
|
||||||
import {IPropertyTemplate} from '../blocks/board'
|
import {IPropertyTemplate} from '../blocks/board'
|
||||||
import {Card, MutableCard} from '../blocks/card'
|
import {Card, MutableCard} from '../blocks/card'
|
||||||
@ -13,7 +14,6 @@ import {Utils} from '../utils'
|
|||||||
import MenuWrapper from '../widgets/menuWrapper'
|
import MenuWrapper from '../widgets/menuWrapper'
|
||||||
|
|
||||||
import {CardDialog} from './cardDialog'
|
import {CardDialog} from './cardDialog'
|
||||||
import {Editable} from './editable'
|
|
||||||
import RootPortal from './rootPortal'
|
import RootPortal from './rootPortal'
|
||||||
import {TableRow} from './tableRow'
|
import {TableRow} from './tableRow'
|
||||||
import ViewHeader from './viewHeader'
|
import ViewHeader from './viewHeader'
|
||||||
@ -21,6 +21,9 @@ import ViewTitle from './viewTitle'
|
|||||||
import TableHeaderMenu from './tableHeaderMenu'
|
import TableHeaderMenu from './tableHeaderMenu'
|
||||||
|
|
||||||
import './tableComponent.scss'
|
import './tableComponent.scss'
|
||||||
|
import {HorizontalGrip} from './horizontalGrip'
|
||||||
|
|
||||||
|
import {MutableBoardView} from '../blocks/boardView'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
boardTree?: BoardTree
|
boardTree?: BoardTree
|
||||||
@ -57,6 +60,7 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {board, cards, activeView} = boardTree
|
const {board, cards, activeView} = boardTree
|
||||||
|
const titleRef = React.createRef<HTMLDivElement>()
|
||||||
|
|
||||||
this.cardIdToRowMap.clear()
|
this.cardIdToRowMap.clear()
|
||||||
|
|
||||||
@ -95,9 +99,10 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
id='mainBoardHeader'
|
id='mainBoardHeader'
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className='octo-table-cell title-cell'
|
|
||||||
style={{overflow: 'unset'}}
|
|
||||||
id='mainBoardHeader'
|
id='mainBoardHeader'
|
||||||
|
ref={titleRef}
|
||||||
|
className='octo-table-cell title-cell header-cell'
|
||||||
|
style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}}
|
||||||
>
|
>
|
||||||
<MenuWrapper>
|
<MenuWrapper>
|
||||||
<div
|
<div
|
||||||
@ -114,23 +119,44 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
templateId='__name'
|
templateId='__name'
|
||||||
/>
|
/>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
|
|
||||||
|
<div className='octo-spacer'/>
|
||||||
|
|
||||||
|
<HorizontalGrip
|
||||||
|
onDrag={(offset) => {
|
||||||
|
const originalWidth = this.columnWidth(Constants.titleColumnId)
|
||||||
|
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||||
|
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)
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
|
{/* Table header row */}
|
||||||
|
|
||||||
{board.cardProperties.
|
{board.cardProperties.
|
||||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||||
map((template) =>
|
map((template) => {
|
||||||
(<div
|
const headerRef = React.createRef<HTMLDivElement>()
|
||||||
|
return (<div
|
||||||
key={template.id}
|
key={template.id}
|
||||||
style={{overflow: 'unset'}}
|
ref={headerRef}
|
||||||
className='octo-table-cell'
|
style={{overflow: 'unset', width: this.columnWidth(template.id)}}
|
||||||
|
className='octo-table-cell header-cell'
|
||||||
draggable={true}
|
|
||||||
onDragStart={() => {
|
|
||||||
this.draggedHeaderTemplate = template
|
|
||||||
}}
|
|
||||||
onDragEnd={() => {
|
|
||||||
this.draggedHeaderTemplate = undefined
|
|
||||||
}}
|
|
||||||
|
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
e.preventDefault(); (e.target as HTMLElement).classList.add('dragover')
|
e.preventDefault(); (e.target as HTMLElement).classList.add('dragover')
|
||||||
@ -149,14 +175,46 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
<div
|
<div
|
||||||
className='octo-label'
|
className='octo-label'
|
||||||
style={{cursor: 'pointer'}}
|
style={{cursor: 'pointer'}}
|
||||||
|
draggable={true}
|
||||||
|
onDragStart={() => {
|
||||||
|
this.draggedHeaderTemplate = template
|
||||||
|
}}
|
||||||
|
onDragEnd={() => {
|
||||||
|
this.draggedHeaderTemplate = undefined
|
||||||
|
}}
|
||||||
>{template.name}</div>
|
>{template.name}</div>
|
||||||
<TableHeaderMenu
|
<TableHeaderMenu
|
||||||
boardTree={boardTree}
|
boardTree={boardTree}
|
||||||
templateId={template.id}
|
templateId={template.id}
|
||||||
/>
|
/>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>),
|
|
||||||
)}
|
<div className='octo-spacer'/>
|
||||||
|
|
||||||
|
<HorizontalGrip
|
||||||
|
onDrag={(offset) => {
|
||||||
|
const originalWidth = this.columnWidth(template.id)
|
||||||
|
const newWidth = Math.max(Constants.minColumnWidth, originalWidth + offset)
|
||||||
|
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)
|
||||||
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Rows, one per card */}
|
{/* Rows, one per card */}
|
||||||
@ -213,6 +271,10 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private columnWidth(templateId: string): number {
|
||||||
|
return Math.max(Constants.minColumnWidth, this.props.boardTree?.activeView?.columnWidths[templateId] || 0)
|
||||||
|
}
|
||||||
|
|
||||||
private addCard = async (show = false) => {
|
private addCard = async (show = false) => {
|
||||||
const {boardTree} = this.props
|
const {boardTree} = this.props
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import {BoardTree} from '../viewModel/boardTree'
|
|||||||
import {Card} from '../blocks/card'
|
import {Card} from '../blocks/card'
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
|
|
||||||
|
import {Constants} from '../constants'
|
||||||
import Editable from '../widgets/editable'
|
import Editable from '../widgets/editable'
|
||||||
import Button from '../widgets/buttons/button'
|
import Button from '../widgets/buttons/button'
|
||||||
|
|
||||||
@ -63,6 +64,7 @@ class TableRow extends React.Component<Props, State> {
|
|||||||
<div
|
<div
|
||||||
className='octo-table-cell title-cell'
|
className='octo-table-cell title-cell'
|
||||||
id='mainBoardHeader'
|
id='mainBoardHeader'
|
||||||
|
style={{width: this.columnWidth(Constants.titleColumnId)}}
|
||||||
>
|
>
|
||||||
<div className='octo-icontitle'>
|
<div className='octo-icontitle'>
|
||||||
<div className='octo-icon'>{card.icon}</div>
|
<div className='octo-icon'>{card.icon}</div>
|
||||||
@ -108,6 +110,7 @@ class TableRow extends React.Component<Props, State> {
|
|||||||
<div
|
<div
|
||||||
className='octo-table-cell'
|
className='octo-table-cell'
|
||||||
key={template.id}
|
key={template.id}
|
||||||
|
style={{width: this.columnWidth(template.id)}}
|
||||||
>
|
>
|
||||||
<PropertyValueElement
|
<PropertyValueElement
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
@ -122,6 +125,10 @@ class TableRow extends React.Component<Props, State> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private columnWidth(templateId: string): number {
|
||||||
|
return Math.max(Constants.minColumnWidth, this.props.boardTree?.activeView?.columnWidths[templateId] || 0)
|
||||||
|
}
|
||||||
|
|
||||||
focusOnTitle(): void {
|
focusOnTitle(): void {
|
||||||
this.titleRef.current?.focus()
|
this.titleRef.current?.focus()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import {BoardTree} from '../viewModel/boardTree'
|
|||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
import Menu from '../widgets/menu'
|
import Menu from '../widgets/menu'
|
||||||
|
import { Constants } from '../constants'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
boardTree?: BoardTree
|
boardTree?: BoardTree
|
||||||
@ -62,6 +63,8 @@ export default class ViewMenu extends React.Component<Props> {
|
|||||||
view.viewType = 'table'
|
view.viewType = 'table'
|
||||||
view.parentId = board.id
|
view.parentId = board.id
|
||||||
view.visiblePropertyIds = board.cardProperties.map((o) => o.id)
|
view.visiblePropertyIds = board.cardProperties.map((o) => o.id)
|
||||||
|
view.columnWidths = {}
|
||||||
|
view.columnWidths[Constants.titleColumnId] = Constants.defaultTitleColumnWidth
|
||||||
|
|
||||||
const oldViewId = boardTree.activeView.id
|
const oldViewId = boardTree.activeView.id
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
class Constants {
|
class Constants {
|
||||||
static menuColors = [
|
static readonly menuColors = [
|
||||||
{id: 'propColorDefault', name: 'Default', type: 'color'},
|
{id: 'propColorDefault', name: 'Default', type: 'color'},
|
||||||
{id: 'propColorGray', name: 'Gray', type: 'color'},
|
{id: 'propColorGray', name: 'Gray', type: 'color'},
|
||||||
{id: 'propColorBrown', name: 'Brown', type: 'color'},
|
{id: 'propColorBrown', name: 'Brown', type: 'color'},
|
||||||
@ -14,6 +14,10 @@ class Constants {
|
|||||||
{id: 'propColorPink', name: 'Pink', type: 'color'},
|
{id: 'propColorPink', name: 'Pink', type: 'color'},
|
||||||
{id: 'propColorRed', name: 'Red', type: 'color'},
|
{id: 'propColorRed', name: 'Red', type: 'color'},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
static readonly minColumnWidth = 100
|
||||||
|
static readonly defaultTitleColumnWidth = 280
|
||||||
|
static readonly titleColumnId = '__title'
|
||||||
}
|
}
|
||||||
|
|
||||||
export {Constants}
|
export {Constants}
|
||||||
|
@ -40,7 +40,7 @@ class Mutator {
|
|||||||
const groupId = this.beginUndoGroup()
|
const groupId = this.beginUndoGroup()
|
||||||
try {
|
try {
|
||||||
await actions()
|
await actions()
|
||||||
} catch(err) {
|
} catch (err) {
|
||||||
Utils.assertFailure(`ERROR: ${err?.toString?.()}`)
|
Utils.assertFailure(`ERROR: ${err?.toString?.()}`)
|
||||||
}
|
}
|
||||||
this.endUndoGroup(groupId)
|
this.endUndoGroup(groupId)
|
||||||
@ -55,7 +55,7 @@ class Mutator {
|
|||||||
await octoClient.updateBlock(oldBlock)
|
await octoClient.updateBlock(oldBlock)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ class Mutator {
|
|||||||
await octoClient.updateBlocks(oldBlocks)
|
await octoClient.updateBlocks(oldBlocks)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ class Mutator {
|
|||||||
await octoClient.deleteBlock(block.id)
|
await octoClient.deleteBlock(block.id)
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +100,7 @@ class Mutator {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,7 +119,7 @@ class Mutator {
|
|||||||
await afterUndo?.()
|
await afterUndo?.()
|
||||||
},
|
},
|
||||||
description,
|
description,
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -491,7 +491,7 @@ class Mutator {
|
|||||||
await octoClient.deleteBlock(block.id)
|
await octoClient.deleteBlock(block.id)
|
||||||
},
|
},
|
||||||
'add image',
|
'add image',
|
||||||
this.undoGroupId
|
this.undoGroupId,
|
||||||
)
|
)
|
||||||
|
|
||||||
return block
|
return block
|
||||||
|
Loading…
x
Reference in New Issue
Block a user