Merge pull request #21 from mattermost/resize-columns
Resize table columns
This commit is contained in:
commit
e55e5fa2a4
9 changed files with 179 additions and 28 deletions
|
@ -17,6 +17,7 @@ interface BoardView extends IBlock {
|
|||
readonly hiddenOptionIds: readonly string[]
|
||||
readonly filter: FilterGroup | undefined
|
||||
readonly cardOrder: readonly string[]
|
||||
readonly columnWidths: Readonly<Record<string, number>>
|
||||
}
|
||||
|
||||
class MutableBoardView extends MutableBlock {
|
||||
|
@ -76,6 +77,13 @@ class MutableBoardView extends MutableBlock {
|
|||
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 = {}) {
|
||||
super(block)
|
||||
|
||||
|
@ -87,6 +95,7 @@ class MutableBoardView extends MutableBlock {
|
|||
this.hiddenOptionIds = block.fields?.hiddenOptionIds?.slice() || []
|
||||
this.filter = new FilterGroup(block.fields?.filter)
|
||||
this.cardOrder = block.fields?.cardOrder?.slice() || []
|
||||
this.columnWidths = {...(block.fields?.columnWidths || {})}
|
||||
|
||||
if (!this.viewType) {
|
||||
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;
|
||||
padding: 5px 8px 6px 8px;
|
||||
|
||||
width: 150px;
|
||||
min-height: 32px;
|
||||
|
||||
font-size: 14px;
|
||||
|
@ -18,13 +17,18 @@
|
|||
position: relative;
|
||||
|
||||
.octo-icontitle {
|
||||
flex: 1 1 auto;
|
||||
.octo-icon {
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.Editable {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.title-cell {
|
||||
width: 280px;
|
||||
&.header-cell {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import React from 'react'
|
||||
import {FormattedMessage} from 'react-intl'
|
||||
|
||||
import {Constants} from '../constants'
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {IPropertyTemplate} from '../blocks/board'
|
||||
import {Card, MutableCard} from '../blocks/card'
|
||||
|
@ -13,7 +14,6 @@ import {Utils} from '../utils'
|
|||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import {CardDialog} from './cardDialog'
|
||||
import {Editable} from './editable'
|
||||
import RootPortal from './rootPortal'
|
||||
import {TableRow} from './tableRow'
|
||||
import ViewHeader from './viewHeader'
|
||||
|
@ -21,6 +21,9 @@ import ViewTitle from './viewTitle'
|
|||
import TableHeaderMenu from './tableHeaderMenu'
|
||||
|
||||
import './tableComponent.scss'
|
||||
import {HorizontalGrip} from './horizontalGrip'
|
||||
|
||||
import {MutableBoardView} from '../blocks/boardView'
|
||||
|
||||
type Props = {
|
||||
boardTree?: BoardTree
|
||||
|
@ -57,6 +60,7 @@ class TableComponent extends React.Component<Props, State> {
|
|||
}
|
||||
|
||||
const {board, cards, activeView} = boardTree
|
||||
const titleRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
this.cardIdToRowMap.clear()
|
||||
|
||||
|
@ -95,9 +99,10 @@ class TableComponent extends React.Component<Props, State> {
|
|||
id='mainBoardHeader'
|
||||
>
|
||||
<div
|
||||
className='octo-table-cell title-cell'
|
||||
style={{overflow: 'unset'}}
|
||||
id='mainBoardHeader'
|
||||
ref={titleRef}
|
||||
className='octo-table-cell title-cell header-cell'
|
||||
style={{overflow: 'unset', width: this.columnWidth(Constants.titleColumnId)}}
|
||||
>
|
||||
<MenuWrapper>
|
||||
<div
|
||||
|
@ -114,23 +119,44 @@ class TableComponent extends React.Component<Props, State> {
|
|||
templateId='__name'
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Table header row */}
|
||||
|
||||
{board.cardProperties.
|
||||
filter((template) => activeView.visiblePropertyIds.includes(template.id)).
|
||||
map((template) =>
|
||||
(<div
|
||||
map((template) => {
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
return (<div
|
||||
key={template.id}
|
||||
style={{overflow: 'unset'}}
|
||||
className='octo-table-cell'
|
||||
|
||||
draggable={true}
|
||||
onDragStart={() => {
|
||||
this.draggedHeaderTemplate = template
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.draggedHeaderTemplate = undefined
|
||||
}}
|
||||
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')
|
||||
|
@ -149,14 +175,46 @@ class TableComponent extends React.Component<Props, State> {
|
|||
<div
|
||||
className='octo-label'
|
||||
style={{cursor: 'pointer'}}
|
||||
draggable={true}
|
||||
onDragStart={() => {
|
||||
this.draggedHeaderTemplate = template
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
this.draggedHeaderTemplate = undefined
|
||||
}}
|
||||
>{template.name}</div>
|
||||
<TableHeaderMenu
|
||||
boardTree={boardTree}
|
||||
templateId={template.id}
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* 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) => {
|
||||
const {boardTree} = this.props
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import {BoardTree} from '../viewModel/boardTree'
|
|||
import {Card} from '../blocks/card'
|
||||
import mutator from '../mutator'
|
||||
|
||||
import {Constants} from '../constants'
|
||||
import Editable from '../widgets/editable'
|
||||
import Button from '../widgets/buttons/button'
|
||||
|
||||
|
@ -63,6 +64,7 @@ class TableRow extends React.Component<Props, State> {
|
|||
<div
|
||||
className='octo-table-cell title-cell'
|
||||
id='mainBoardHeader'
|
||||
style={{width: this.columnWidth(Constants.titleColumnId)}}
|
||||
>
|
||||
<div className='octo-icontitle'>
|
||||
<div className='octo-icon'>{card.icon}</div>
|
||||
|
@ -108,6 +110,7 @@ class TableRow extends React.Component<Props, State> {
|
|||
<div
|
||||
className='octo-table-cell'
|
||||
key={template.id}
|
||||
style={{width: this.columnWidth(template.id)}}
|
||||
>
|
||||
<PropertyValueElement
|
||||
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 {
|
||||
this.titleRef.current?.focus()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import {BoardTree} from '../viewModel/boardTree'
|
|||
import mutator from '../mutator'
|
||||
import {Utils} from '../utils'
|
||||
import Menu from '../widgets/menu'
|
||||
import { Constants } from '../constants'
|
||||
|
||||
type Props = {
|
||||
boardTree?: BoardTree
|
||||
|
@ -62,6 +63,8 @@ export default class ViewMenu extends React.Component<Props> {
|
|||
view.viewType = 'table'
|
||||
view.parentId = board.id
|
||||
view.visiblePropertyIds = board.cardProperties.map((o) => o.id)
|
||||
view.columnWidths = {}
|
||||
view.columnWidths[Constants.titleColumnId] = Constants.defaultTitleColumnWidth
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
class Constants {
|
||||
static menuColors = [
|
||||
static readonly menuColors = [
|
||||
{id: 'propColorDefault', name: 'Default', type: 'color'},
|
||||
{id: 'propColorGray', name: 'Gray', type: 'color'},
|
||||
{id: 'propColorBrown', name: 'Brown', type: 'color'},
|
||||
|
@ -14,6 +14,10 @@ class Constants {
|
|||
{id: 'propColorPink', name: 'Pink', type: 'color'},
|
||||
{id: 'propColorRed', name: 'Red', type: 'color'},
|
||||
]
|
||||
|
||||
static readonly minColumnWidth = 100
|
||||
static readonly defaultTitleColumnWidth = 280
|
||||
static readonly titleColumnId = '__title'
|
||||
}
|
||||
|
||||
export {Constants}
|
||||
|
|
|
@ -40,7 +40,7 @@ class Mutator {
|
|||
const groupId = this.beginUndoGroup()
|
||||
try {
|
||||
await actions()
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
Utils.assertFailure(`ERROR: ${err?.toString?.()}`)
|
||||
}
|
||||
this.endUndoGroup(groupId)
|
||||
|
@ -55,7 +55,7 @@ class Mutator {
|
|||
await octoClient.updateBlock(oldBlock)
|
||||
},
|
||||
description,
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@ class Mutator {
|
|||
await octoClient.updateBlocks(oldBlocks)
|
||||
},
|
||||
description,
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -83,7 +83,7 @@ class Mutator {
|
|||
await octoClient.deleteBlock(block.id)
|
||||
},
|
||||
description,
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ class Mutator {
|
|||
}
|
||||
},
|
||||
description,
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,7 @@ class Mutator {
|
|||
await afterUndo?.()
|
||||
},
|
||||
description,
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -491,7 +491,7 @@ class Mutator {
|
|||
await octoClient.deleteBlock(block.id)
|
||||
},
|
||||
'add image',
|
||||
this.undoGroupId
|
||||
this.undoGroupId,
|
||||
)
|
||||
|
||||
return block
|
||||
|
|
Loading…
Reference in a new issue