focalboard/webapp/src/components/cardDetail.tsx

306 lines
12 KiB
TypeScript
Raw Normal View History

2020-10-20 21:50:53 +02:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
2020-10-20 21:52:56 +02:00
import React from 'react'
2021-01-19 23:48:20 +01:00
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
2020-10-21 03:28:55 +02:00
import {BlockIcons} from '../blockIcons'
2021-03-10 23:55:01 +01:00
import {BlockTypes} from '../blocks/block'
2020-11-03 19:35:24 +01:00
import {PropertyType} from '../blocks/board'
2021-01-19 23:48:20 +01:00
import {MutableTextBlock} from '../blocks/textBlock'
import mutator from '../mutator'
import {Utils} from '../utils'
2021-01-19 23:48:20 +01:00
import {BoardTree} from '../viewModel/boardTree'
import {CardTree} from '../viewModel/cardTree'
2020-10-27 22:04:38 +01:00
import Button from '../widgets/buttons/button'
2021-01-19 23:48:20 +01:00
import Editable from '../widgets/editable'
import EmojiIcon from '../widgets/icons/emoji'
2021-01-19 23:48:20 +01:00
import Menu from '../widgets/menu'
import MenuWrapper from '../widgets/menuWrapper'
import PropertyMenu from '../widgets/propertyMenu'
import BlockIconSelector from './blockIconSelector'
import './cardDetail.scss'
2021-01-19 23:48:20 +01:00
import CommentsList from './commentsList'
2021-03-10 23:55:01 +01:00
import {ContentHandler, contentRegistry} from './content/contentRegistry'
2021-01-19 23:48:20 +01:00
import ContentBlock from './contentBlock'
import {MarkdownEditor} from './markdownEditor'
import PropertyValueElement from './propertyValueElement'
type Props = {
2020-10-20 21:50:53 +02:00
boardTree: BoardTree
2020-11-11 18:21:16 +01:00
cardTree: CardTree
intl: IntlShape
2020-12-17 21:02:12 +01:00
readonly: boolean
}
type State = {
title: string
}
class CardDetail extends React.Component<Props, State> {
2020-10-20 21:50:53 +02:00
private titleRef = React.createRef<Editable>()
2020-11-12 23:06:19 +01:00
shouldComponentUpdate(): boolean {
2020-10-25 14:40:47 +01:00
return true
}
2020-11-12 23:06:19 +01:00
componentDidMount(): void {
if (!this.state.title) {
this.titleRef.current?.focus()
}
2020-11-12 23:06:19 +01:00
}
2021-03-26 12:41:09 +01:00
componentWillUnmount(): void {
const {cardTree} = this.props
if (!cardTree) {
return
}
const {card} = cardTree
if (this.state.title !== card.title) {
mutator.changeTitle(card, this.state.title)
}
}
2020-10-20 21:50:53 +02:00
constructor(props: Props) {
super(props)
this.state = {
2020-11-11 18:21:16 +01:00
title: props.cardTree.card.title,
}
2020-10-20 21:50:53 +02:00
}
render() {
2021-03-11 00:14:18 +01:00
const {boardTree, cardTree} = this.props
2020-10-20 21:50:53 +02:00
const {board} = boardTree
if (!cardTree) {
return null
2020-10-20 21:50:53 +02:00
}
2020-10-21 03:28:55 +02:00
const {card, comments} = cardTree
2020-10-20 21:50:53 +02:00
let contentElements
if (cardTree.contents.length > 0) {
contentElements =
(<div className='octo-content'>
2020-10-25 15:36:49 +01:00
{cardTree.contents.map((block) => (
<ContentBlock
key={block.id}
block={block}
card={card}
contents={cardTree.contents}
2020-12-17 21:02:12 +01:00
readonly={this.props.readonly}
2020-10-25 15:36:49 +01:00
/>
))}
2020-10-20 21:50:53 +02:00
</div>)
} else {
2020-10-20 21:50:53 +02:00
contentElements = (<div className='octo-content'>
<div className='octo-block'>
2020-10-20 21:50:53 +02:00
<div className='octo-block-margin'/>
2020-12-17 21:02:12 +01:00
{!this.props.readonly &&
<MarkdownEditor
text=''
placeholderText='Add a description...'
onBlur={(text) => {
if (text) {
2020-12-18 21:52:45 +01:00
this.addTextBlock(text)
2020-12-17 21:02:12 +01:00
}
}}
/>
}
2020-10-20 21:50:53 +02:00
</div>
</div>)
}
2020-10-20 21:50:53 +02:00
const icon = card.icon
return (
2020-10-20 21:50:53 +02:00
<>
<div className='CardDetail content'>
<BlockIconSelector
block={card}
size='l'
2020-12-17 21:02:12 +01:00
readonly={this.props.readonly}
/>
2020-12-17 21:02:12 +01:00
{!this.props.readonly && !icon &&
<div className='add-buttons'>
<Button
onClick={() => {
const newIcon = BlockIcons.shared.randomIcon()
mutator.changeIcon(card, newIcon)
2020-10-27 22:00:15 +01:00
}}
icon={<EmojiIcon/>}
2020-10-27 22:00:15 +01:00
>
<FormattedMessage
id='CardDetail.add-icon'
2020-12-11 20:10:25 +01:00
defaultMessage='Add icon'
/>
</Button>
</div>}
2020-10-20 21:50:53 +02:00
<Editable
ref={this.titleRef}
className='title'
value={this.state.title}
placeholderText='Untitled'
onChange={(title: string) => this.setState({title})}
2020-11-10 22:19:46 +01:00
saveOnEsc={true}
2020-11-09 21:05:55 +01:00
onSave={() => {
2020-11-11 18:21:16 +01:00
if (this.state.title !== this.props.cardTree.card.title) {
2020-11-09 21:05:55 +01:00
mutator.changeTitle(card, this.state.title)
}
}}
2020-11-11 18:21:16 +01:00
onCancel={() => this.setState({title: this.props.cardTree.card.title})}
2020-12-17 21:02:12 +01:00
readonly={this.props.readonly}
/>
2020-10-20 21:50:53 +02:00
{/* Property list */}
<div className='octo-propertylist'>
{board.cardProperties.map((propertyTemplate) => {
2021-02-23 02:01:07 +01:00
const propertyValue = card.properties[propertyTemplate.id]
2020-10-20 21:50:53 +02:00
return (
<div
2021-02-23 02:01:07 +01:00
key={propertyTemplate.id + '-' + propertyTemplate.type + '-' + propertyValue}
2020-10-20 21:50:53 +02:00
className='octo-propertyrow'
>
2020-12-17 21:02:12 +01:00
{this.props.readonly && <div className='octo-propertyname'>{propertyTemplate.name}</div>}
{!this.props.readonly &&
<MenuWrapper>
<div className='octo-propertyname'><Button>{propertyTemplate.name}</Button></div>
<PropertyMenu
propertyId={propertyTemplate.id}
propertyName={propertyTemplate.name}
propertyType={propertyTemplate.type}
onNameChanged={(newName: string) => mutator.renameProperty(board, propertyTemplate.id, newName)}
onTypeChanged={(newType: PropertyType) => mutator.changePropertyType(boardTree, propertyTemplate, newType)}
onDelete={(id: string) => mutator.deleteProperty(boardTree, id)}
/>
</MenuWrapper>
}
2020-10-26 12:43:16 +01:00
<PropertyValueElement
2020-12-17 21:02:12 +01:00
readOnly={this.props.readonly}
2020-10-26 12:43:16 +01:00
card={card}
2020-10-30 15:22:11 +01:00
boardTree={boardTree}
2020-10-26 12:43:16 +01:00
propertyTemplate={propertyTemplate}
emptyDisplayValue='Empty'
/>
2020-10-20 21:50:53 +02:00
</div>
)
2020-10-20 21:50:53 +02:00
})}
2020-12-17 21:02:12 +01:00
{!this.props.readonly &&
<div className='octo-propertyname add-property'>
<Button
onClick={async () => {
// TODO: Show UI
await mutator.insertPropertyTemplate(boardTree)
}}
>
<FormattedMessage
id='CardDetail.add-property'
defaultMessage='+ Add a property'
/>
</Button>
</div>
}
</div>
2020-10-20 21:50:53 +02:00
{/* Comments */}
2020-12-17 21:02:12 +01:00
{!this.props.readonly &&
<>
<hr/>
2021-01-20 18:47:08 +01:00
<CommentsList
comments={comments}
rootId={card.rootId}
cardId={card.id}
/>
2020-12-17 21:02:12 +01:00
<hr/>
</>
}
2020-10-20 21:50:53 +02:00
</div>
{/* Content blocks */}
2020-10-20 21:50:53 +02:00
<div className='CardDetail content fullwidth'>
2020-10-20 21:50:53 +02:00
{contentElements}
</div>
2020-12-17 21:02:12 +01:00
{!this.props.readonly &&
<div className='CardDetail content add-content'>
<MenuWrapper>
<Button>
<FormattedMessage
id='CardDetail.add-content'
defaultMessage='Add content'
/>
</Button>
<Menu position='top'>
2021-03-10 23:55:01 +01:00
{contentRegistry.contentTypes.map((type) => this.addContentMenu(type))}
2020-12-17 21:02:12 +01:00
</Menu>
</MenuWrapper>
</div>
}
</>
2020-10-20 21:50:53 +02:00
)
}
2020-12-18 21:52:45 +01:00
2021-03-10 23:55:01 +01:00
private addContentMenu(type: BlockTypes): JSX.Element {
const {intl} = this.props
const handler = contentRegistry.getHandler(type)
if (!handler) {
Utils.logError(`addContentMenu, unknown content type: ${type}`)
return <></>
}
return (
<Menu.Text
2021-03-11 00:00:54 +01:00
key={type}
2021-03-10 23:55:01 +01:00
id={type}
name={handler.getDisplayText(intl)}
icon={handler.getIcon()}
onClick={() => {
this.addBlock(handler)
}}
/>
)
}
private async addBlock(handler: ContentHandler) {
const {intl, cardTree} = this.props
const {card} = cardTree
const newBlock = await handler.createBlock()
newBlock.parentId = card.id
newBlock.rootId = card.rootId
const contentOrder = card.contentOrder.slice()
contentOrder.push(newBlock.id)
const typeName = handler.getDisplayText(intl)
const description = intl.formatMessage({id: 'ContentBlock.addElement', defaultMessage: 'add {type}'}, {type: typeName})
mutator.performAsUndoGroup(async () => {
await mutator.insertBlock(newBlock, description)
await mutator.changeCardContentOrder(card, contentOrder, description)
})
}
2020-12-18 21:52:45 +01:00
private addTextBlock(text: string): void {
const {intl, cardTree} = this.props
const {card} = cardTree
const block = new MutableTextBlock()
block.parentId = card.id
block.rootId = card.rootId
block.title = text
const contentOrder = card.contentOrder.slice()
contentOrder.push(block.id)
mutator.performAsUndoGroup(async () => {
const description = intl.formatMessage({id: 'CardDetail.addCardText', defaultMessage: 'add card text'})
await mutator.insertBlock(block, description)
await mutator.changeCardContentOrder(card, contentOrder, description)
})
}
}
export default injectIntl(CardDetail)