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
|
|
|
|
2020-10-22 00:03:12 +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'
|
2020-10-22 00:03:12 +02:00
|
|
|
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'
|
2020-10-31 16:41:11 +01:00
|
|
|
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'
|
2020-10-25 13:09:15 +01:00
|
|
|
|
2020-10-31 16:41:11 +01:00
|
|
|
import BlockIconSelector from './blockIconSelector'
|
2020-10-25 13:09:15 +01:00
|
|
|
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'
|
2020-10-25 13:09:15 +01:00
|
|
|
|
2020-10-15 19:46:32 +02:00
|
|
|
type Props = {
|
2020-10-20 21:50:53 +02:00
|
|
|
boardTree: BoardTree
|
2020-11-11 18:21:16 +01:00
|
|
|
cardTree: CardTree
|
2020-10-25 13:09:15 +01:00
|
|
|
intl: IntlShape
|
2020-12-17 21:02:12 +01:00
|
|
|
readonly: boolean
|
2020-10-15 19:46:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type State = {
|
2020-10-26 18:05:02 +01:00
|
|
|
title: string
|
2020-10-15 19:46:32 +02:00
|
|
|
}
|
|
|
|
|
2020-10-25 13:09:15 +01:00
|
|
|
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 {
|
2020-11-20 20:54:40 +01:00
|
|
|
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) {
|
2020-10-22 00:03:12 +02:00
|
|
|
super(props)
|
2020-10-26 18:05:02 +01:00
|
|
|
this.state = {
|
2020-11-11 18:21:16 +01:00
|
|
|
title: props.cardTree.card.title,
|
2020-10-26 18:05:02 +01:00
|
|
|
}
|
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
|
2020-10-22 00:03:12 +02:00
|
|
|
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}
|
2020-12-03 23:09:48 +01:00
|
|
|
card={card}
|
2020-11-09 22:06:39 +01:00
|
|
|
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>)
|
2020-10-22 00:03:12 +02:00
|
|
|
} else {
|
2020-10-20 21:50:53 +02:00
|
|
|
contentElements = (<div className='octo-content'>
|
2020-11-02 10:46:28 +01:00
|
|
|
<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-22 00:03:12 +02:00
|
|
|
}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
const icon = card.icon
|
|
|
|
|
2020-10-22 00:03:12 +02:00
|
|
|
return (
|
2020-10-20 21:50:53 +02:00
|
|
|
<>
|
2020-10-25 13:09:15 +01:00
|
|
|
<div className='CardDetail content'>
|
2020-10-31 16:41:11 +01:00
|
|
|
<BlockIconSelector
|
|
|
|
block={card}
|
|
|
|
size='l'
|
2020-12-17 21:02:12 +01:00
|
|
|
readonly={this.props.readonly}
|
2020-10-31 16:41:11 +01:00
|
|
|
/>
|
2020-12-17 21:02:12 +01:00
|
|
|
{!this.props.readonly && !icon &&
|
2020-11-02 10:46:28 +01:00
|
|
|
<div className='add-buttons'>
|
2020-10-25 13:09:15 +01:00
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
const newIcon = BlockIcons.shared.randomIcon()
|
|
|
|
mutator.changeIcon(card, newIcon)
|
2020-10-27 22:00:15 +01:00
|
|
|
}}
|
2020-10-31 16:41:11 +01:00
|
|
|
icon={<EmojiIcon/>}
|
2020-10-27 22:00:15 +01:00
|
|
|
>
|
2020-10-25 13:09:15 +01:00
|
|
|
<FormattedMessage
|
|
|
|
id='CardDetail.add-icon'
|
2020-12-11 20:10:25 +01:00
|
|
|
defaultMessage='Add icon'
|
2020-10-25 13:09:15 +01:00
|
|
|
/>
|
|
|
|
</Button>
|
|
|
|
</div>}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
<Editable
|
2020-10-22 00:03:12 +02:00
|
|
|
ref={this.titleRef}
|
|
|
|
className='title'
|
2020-10-26 18:05:02 +01:00
|
|
|
value={this.state.title}
|
2020-10-22 00:03:12 +02:00
|
|
|
placeholderText='Untitled'
|
2020-10-26 18:05:02 +01:00
|
|
|
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-22 00:03:12 +02:00
|
|
|
/>
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
{/* Property list */}
|
|
|
|
|
|
|
|
<div className='octo-propertylist'>
|
2020-10-22 00:03:12 +02:00
|
|
|
{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-22 00:03:12 +02:00
|
|
|
)
|
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>
|
|
|
|
}
|
2020-10-22 00:03:12 +02:00
|
|
|
</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>
|
|
|
|
|
2020-10-22 00:03:12 +02:00
|
|
|
{/* Content blocks */}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
2020-10-25 13:09:15 +01: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-22 00:03:12 +02:00
|
|
|
</>
|
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)
|
|
|
|
})
|
|
|
|
}
|
2020-10-15 19:46:32 +02:00
|
|
|
}
|
2020-10-25 13:09:15 +01:00
|
|
|
|
|
|
|
export default injectIntl(CardDetail)
|