diff --git a/webapp/src/blocks/boardView.ts b/webapp/src/blocks/boardView.ts index a0404095f..545846da3 100644 --- a/webapp/src/blocks/boardView.ts +++ b/webapp/src/blocks/boardView.ts @@ -17,6 +17,7 @@ interface BoardView extends IBlock { readonly hiddenOptionIds: readonly string[] readonly filter: FilterGroup | undefined readonly cardOrder: readonly string[] + readonly columnWidths: Readonly> } class MutableBoardView extends MutableBlock { @@ -76,6 +77,13 @@ class MutableBoardView extends MutableBlock { this.fields.cardOrder = value } + get columnWidths(): Record { + return this.fields.columnWidths as Record + } + set columnWidths(value: Record) { + 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' diff --git a/webapp/src/components/horizontalGrip.scss b/webapp/src/components/horizontalGrip.scss new file mode 100644 index 000000000..baeb3d9f6 --- /dev/null +++ b/webapp/src/components/horizontalGrip.scss @@ -0,0 +1,8 @@ +.HorizontalGrip { + width: 5px; + cursor: ew-resize; + + &:hover { + background-color: rgba(90, 192, 255, 0.7); + } +} diff --git a/webapp/src/components/horizontalGrip.tsx b/webapp/src/components/horizontalGrip.tsx new file mode 100644 index 000000000..2473676c4 --- /dev/null +++ b/webapp/src/components/horizontalGrip.tsx @@ -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 { + state: State = { + } + + render(): JSX.Element { + return ( +
{ + 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} diff --git a/webapp/src/components/tableComponent.scss b/webapp/src/components/tableComponent.scss index 1984f7d49..19b24eb66 100644 --- a/webapp/src/components/tableComponent.scss +++ b/webapp/src/components/tableComponent.scss @@ -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 { diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 0bd033bd4..c62672fba 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -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 { } const {board, cards, activeView} = boardTree + const titleRef = React.createRef() this.cardIdToRowMap.clear() @@ -95,9 +99,10 @@ class TableComponent extends React.Component { id='mainBoardHeader' >
{ templateId='__name' /> + +
+ + { + 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') + } + }} + />
+ {/* Table header row */} + {board.cardProperties. filter((template) => activeView.visiblePropertyIds.includes(template.id)). - map((template) => - (
{ + const headerRef = React.createRef() + return (
{ - 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 {
{ + this.draggedHeaderTemplate = template + }} + onDragEnd={() => { + this.draggedHeaderTemplate = undefined + }} >{template.name}
-
), - )} + +
+ + { + 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') + } + }} + /> +
) + })}
{/* Rows, one per card */} @@ -213,6 +271,10 @@ class TableComponent extends React.Component { ) } + 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 diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index 0ceb109ce..26a8253f0 100644 --- a/webapp/src/components/tableRow.tsx +++ b/webapp/src/components/tableRow.tsx @@ -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 {
{card.icon}
@@ -108,6 +110,7 @@ class TableRow extends React.Component {
{ ) } + private columnWidth(templateId: string): number { + return Math.max(Constants.minColumnWidth, this.props.boardTree?.activeView?.columnWidths[templateId] || 0) + } + focusOnTitle(): void { this.titleRef.current?.focus() } diff --git a/webapp/src/components/viewMenu.tsx b/webapp/src/components/viewMenu.tsx index a470a36ca..892ffb6f0 100644 --- a/webapp/src/components/viewMenu.tsx +++ b/webapp/src/components/viewMenu.tsx @@ -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 { 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 diff --git a/webapp/src/constants.ts b/webapp/src/constants.ts index 774dbe856..25fc0c83a 100644 --- a/webapp/src/constants.ts +++ b/webapp/src/constants.ts @@ -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} diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index f15e0a453..db5c35840 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -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