Creating a separate table component

This commit is contained in:
Jesús Espino 2021-03-27 08:29:31 +01:00
parent 7a50a9339e
commit 24c66d6435
7 changed files with 427 additions and 380 deletions

View file

@ -0,0 +1,118 @@
.Table {
.octo-table-cell {
flex: 0 0 auto;
display: flex;
flex-direction: row;
color: rgb(var(--body-color));
border-right: solid 1px rgba(var(--body-color), 0.09);
box-sizing: border-box;
padding: 8px;
min-height: 32px;
font-size: 14px;
line-height: 20px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: rgba(var(--body-color), 0.05);
}
&.title-cell {
padding-left: 16px;
}
.octo-icontitle {
flex: 1 1 auto;
.octo-icon {
min-width: 20px;
}
.Editable {
flex: 1 1 auto;
}
}
&.header-cell {
padding-right: 0;
.Icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-left: 5px;
}
}
&:focus-within {
background-color: rgba(46, 170, 220, 0.15);
border: 1px solid rgba(46, 170, 220, 0.6);
}
.octo-editable {
padding: 0 5px;
display: relative;
left: -5px;
}
.octo-editable.octo-editable.active {
overflow: hidden;
}
.octo-propertyvalue {
line-height: 17px;
overflow: hidden;
text-overflow: ellipsis;
}
.octo-editable,
.octo-propertyvalue {
text-align: left;
white-space: nowrap;
}
}
.octo-table-body {
display: flex;
flex-direction: column;
}
.octo-table-header,
.octo-table-row,
.octo-table-footer {
display: flex;
flex-direction: row;
border-bottom: solid 1px rgba(var(--body-color), 0.09);
}
.octo-table-header {
.octo-table-cell {
color: rgba(var(--body-color), 0.6);
.MenuWrapper {
overflow: hidden;
}
.octo-label {
color: rgba(var(--body-color), 0.6);
}
}
}
.octo-table-footer {
.octo-table-cell {
color: rgba(var(--body-color), 0.6);
cursor: pointer;
width: 100%;
border-right: none;
padding-left: 15px;
&:hover {
background-color: rgba(var(--body-color), 0.08);
}
}
}
}

View file

@ -0,0 +1,283 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage, injectIntl} from 'react-intl'
import {IPropertyTemplate} from '../../blocks/board'
import {MutableBoardView} from '../../blocks/boardView'
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 {HorizontalGrip} from '../horizontalGrip'
import './table.scss'
import TableHeaderMenu from './tableHeaderMenu'
import TableRow from './tableRow'
type Props = {
boardTree: BoardTree
readonly: boolean
cardIdToFocusOnRender: string
showCard: (cardId?: string) => void
addCard: (show?: boolean) => Promise<void>
}
type State = {
shownCardId?: string
}
class Table extends React.Component<Props, State> {
private draggedHeaderTemplate?: IPropertyTemplate
private cardIdToRowMap = new Map<string, React.RefObject<TableRow>>()
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/>
}
this.cardIdToRowMap.clear()
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}>
<div
className='octo-label'
>
<FormattedMessage
id='TableComponent.name'
defaultMessage='Name'
/>
{titleSortIcon}
</div>
<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
className='octo-label'
draggable={!this.props.readonly}
onDragStart={() => {
this.draggedHeaderTemplate = template
}}
onDragEnd={() => {
this.draggedHeaderTemplate = undefined
}}
>
{template.name}
{sortIcon}
</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 tableRowRef = React.createRef<TableRow>()
const tableRow = (
<TableRow
key={card.id + card.updateAt}
ref={tableRowRef}
boardTree={boardTree}
card={card}
focusOnMount={this.props.cardIdToFocusOnRender === card.id}
onSaveWithEnter={() => {
if (cards.length > 0 && cards[cards.length - 1] === card) {
this.props.addCard(false)
}
}}
showCard={this.props.showCard}
readonly={this.props.readonly}
/>)
this.cardIdToRowMap.set(card.id, tableRowRef)
return tableRow
})}
{/* Add New row */}
<div className='octo-table-footer'>
{!this.props.readonly &&
<div
className='octo-table-cell'
onClick={() => {
this.props.addCard()
}}
>
<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
}
const {boardTree} = this.props
const {board} = boardTree
Utils.assertValue(mutator)
Utils.assertValue(boardTree)
Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`)
// Move template to new index
const destIndex = template ? board.cardProperties.indexOf(template) : 0
await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex)
}
}
export default Table

View file

@ -4,10 +4,10 @@
import React, {FC} from 'react'
import {injectIntl, IntlShape} from 'react-intl'
import {Constants} from '../constants'
import mutator from '../mutator'
import {BoardTree} from '../viewModel/boardTree'
import Menu from '../widgets/menu'
import {Constants} from '../../constants'
import mutator from '../../mutator'
import {BoardTree} from '../../viewModel/boardTree'
import Menu from '../../widgets/menu'
type Props = {
templateId: string

View file

@ -3,14 +3,14 @@
import React from 'react'
import {FormattedMessage} from 'react-intl'
import {Card} from '../blocks/card'
import {Constants} from '../constants'
import mutator from '../mutator'
import {BoardTree} from '../viewModel/boardTree'
import Button from '../widgets/buttons/button'
import Editable from '../widgets/editable'
import {Card} from '../../blocks/card'
import {Constants} from '../../constants'
import mutator from '../../mutator'
import {BoardTree} from '../../viewModel/boardTree'
import Button from '../../widgets/buttons/button'
import Editable from '../../widgets/editable'
import PropertyValueElement from './propertyValueElement'
import PropertyValueElement from '../propertyValueElement'
import './tableRow.scss'
type Props = {
@ -122,4 +122,4 @@ class TableRow extends React.Component<Props, State> {
}
}
export {TableRow}
export default TableRow

View file

@ -1,119 +1,2 @@
.TableComponent {
.octo-table-cell {
flex: 0 0 auto;
display: flex;
flex-direction: row;
color: rgb(var(--body-color));
border-right: solid 1px rgba(var(--body-color), 0.09);
box-sizing: border-box;
padding: 8px;
min-height: 32px;
font-size: 14px;
line-height: 20px;
position: relative;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: rgba(var(--body-color), 0.05);
}
&.title-cell {
padding-left: 16px;
}
.octo-icontitle {
flex: 1 1 auto;
.octo-icon {
min-width: 20px;
}
.Editable {
flex: 1 1 auto;
}
}
&.header-cell {
padding-right: 0;
.Icon {
width: 16px;
height: 16px;
vertical-align: middle;
margin-left: 5px;
}
}
&:focus-within {
background-color: rgba(46, 170, 220, 0.15);
border: 1px solid rgba(46, 170, 220, 0.6);
}
.octo-editable {
padding: 0 5px;
display: relative;
left: -5px;
}
.octo-editable.octo-editable.active {
overflow: hidden;
}
.octo-propertyvalue {
line-height: 17px;
overflow: hidden;
text-overflow: ellipsis;
}
.octo-editable,
.octo-propertyvalue {
text-align: left;
white-space: nowrap;
}
}
.octo-table-body {
display: flex;
flex-direction: column;
}
.octo-table-header,
.octo-table-row,
.octo-table-footer {
display: flex;
flex-direction: row;
border-bottom: solid 1px rgba(var(--body-color), 0.09);
}
.octo-table-header {
.octo-table-cell {
color: rgba(var(--body-color), 0.6);
.MenuWrapper {
overflow: hidden;
}
.octo-label {
color: rgba(var(--body-color), 0.6);
}
}
}
.octo-table-footer {
.octo-table-cell {
color: rgba(var(--body-color), 0.6);
cursor: pointer;
width: 100%;
border-right: none;
padding-left: 15px;
&:hover {
background-color: rgba(var(--body-color), 0.08);
}
}
}
}

View file

@ -1,29 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react'
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
import {injectIntl, IntlShape} from 'react-intl'
import {BlockIcons} from '../blockIcons'
import {IPropertyTemplate} from '../blocks/board'
import {MutableBoardView} from '../blocks/boardView'
import {MutableCard} from '../blocks/card'
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 CardDialog from './cardDialog'
import {HorizontalGrip} from './horizontalGrip'
import RootPortal from './rootPortal'
import './tableComponent.scss'
import TableHeaderMenu from './tableHeaderMenu'
import {TableRow} from './tableRow'
import TopBar from './topBar'
import ViewHeader from './viewHeader'
import ViewTitle from './viewTitle'
import Table from './table/table'
type Props = {
boardTree: BoardTree
@ -35,13 +28,11 @@ type Props = {
type State = {
shownCardId?: string
cardIdToFocusOnRender: string
}
class TableComponent extends React.Component<Props, State> {
private draggedHeaderTemplate?: IPropertyTemplate
private cardIdToRowMap = new Map<string, React.RefObject<TableRow>>()
private cardIdToFocusOnRender?: string
state: State = {}
state: State = {cardIdToFocusOnRender: ''}
shouldComponentUpdate(): boolean {
return true
@ -61,16 +52,7 @@ class TableComponent extends React.Component<Props, State> {
render(): JSX.Element {
const {boardTree, showView} = this.props
const {board, cards, activeView} = boardTree
const titleRef = React.createRef<HTMLDivElement>()
let titleSortIcon
const titleSortOption = activeView.sortOptions.find((o) => o.propertyId === Constants.titleColumnId)
if (titleSortOption) {
titleSortIcon = titleSortOption.reversed ? <SortUpIcon/> : <SortDownIcon/>
}
this.cardIdToRowMap.clear()
const {board} = boardTree
return (
<div className='TableComponent octo-app'>
@ -106,214 +88,13 @@ class TableComponent extends React.Component<Props, State> {
/>
{/* Main content */}
<div className='octo-table-body'>
{/* 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}
>
<div
className='octo-label'
>
<FormattedMessage
id='TableComponent.name'
defaultMessage='Name'
/>
{titleSortIcon}
</div>
<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
className='octo-label'
draggable={!this.props.readonly}
onDragStart={() => {
this.draggedHeaderTemplate = template
}}
onDragEnd={() => {
this.draggedHeaderTemplate = undefined
}}
>
{template.name}
{sortIcon}
</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 tableRowRef = React.createRef<TableRow>()
let focusOnMount = false
if (this.cardIdToFocusOnRender && this.cardIdToFocusOnRender === card.id) {
this.cardIdToFocusOnRender = undefined
focusOnMount = true
}
const tableRow = (
<TableRow
key={card.id + card.updateAt}
ref={tableRowRef}
boardTree={boardTree}
card={card}
focusOnMount={focusOnMount}
onSaveWithEnter={() => {
if (cards.length > 0 && cards[cards.length - 1] === card) {
this.addCard(false)
}
}}
showCard={this.showCard}
readonly={this.props.readonly}
/>)
this.cardIdToRowMap.set(card.id, tableRowRef)
return tableRow
})}
{/* Add New row */}
<div className='octo-table-footer'>
{!this.props.readonly &&
<div
className='octo-table-cell'
onClick={() => {
this.addCard()
}}
>
<FormattedMessage
id='TableComponent.plus-new'
defaultMessage='+ New'
/>
</div>
}
</div>
</div>
<Table
boardTree={boardTree}
readonly={this.props.readonly}
cardIdToFocusOnRender={this.state.cardIdToFocusOnRender}
showCard={this.showCard}
addCard={this.addCard}
/>
</div>
</div >
</div >
@ -365,7 +146,8 @@ class TableComponent extends React.Component<Props, State> {
this.showCard(card.id)
} else {
// Focus on this card's title inline on next render
this.cardIdToFocusOnRender = card.id
this.setState({cardIdToFocusOnRender: card.id})
setTimeout(() => this.setState({cardIdToFocusOnRender: ''}), 100)
}
},
)
@ -392,25 +174,6 @@ class TableComponent extends React.Component<Props, State> {
private editCardTemplate = (cardTemplateId: string) => {
this.showCard(cardTemplateId)
}
private async onDropToColumn(template: IPropertyTemplate) {
const {draggedHeaderTemplate} = this
if (!draggedHeaderTemplate) {
return
}
const {boardTree} = this.props
const {board} = boardTree
Utils.assertValue(mutator)
Utils.assertValue(boardTree)
Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`)
// Move template to new index
const destIndex = template ? board.cardProperties.indexOf(template) : 0
await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex)
}
}
export default injectIntl(TableComponent)