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'
|
2020-10-25 13:09:15 +01:00
|
|
|
import {FormattedMessage, IntlShape, injectIntl} from 'react-intl'
|
2020-10-21 03:28:55 +02:00
|
|
|
|
2020-10-22 00:03:12 +02:00
|
|
|
import {BlockIcons} from '../blockIcons'
|
|
|
|
import {MutableCommentBlock} from '../blocks/commentBlock'
|
|
|
|
import {MutableTextBlock} from '../blocks/textBlock'
|
|
|
|
import {BoardTree} from '../viewModel/boardTree'
|
|
|
|
import {CardTree, MutableCardTree} from '../viewModel/cardTree'
|
|
|
|
import mutator from '../mutator'
|
|
|
|
import {OctoListener} from '../octoListener'
|
|
|
|
import {OctoUtils} from '../octoUtils'
|
|
|
|
import {PropertyMenu} from '../propertyMenu'
|
|
|
|
import {Utils} from '../utils'
|
2020-10-21 03:28:55 +02:00
|
|
|
|
2020-10-25 13:09:15 +01:00
|
|
|
import MenuWrapper from '../widgets/menuWrapper'
|
|
|
|
import Menu from '../widgets/menu'
|
|
|
|
|
2020-10-22 00:03:12 +02:00
|
|
|
import Button from './button'
|
|
|
|
import {Editable} from './editable'
|
|
|
|
import {MarkdownEditor} from './markdownEditor'
|
2020-10-25 15:36:49 +01:00
|
|
|
import ContentBlock from './contentBlock'
|
2020-10-25 15:52:46 +01:00
|
|
|
import CommentsList from './commentsList'
|
2020-10-15 19:46:32 +02:00
|
|
|
|
2020-10-25 13:09:15 +01:00
|
|
|
import './cardDetail.scss'
|
|
|
|
|
2020-10-15 19:46:32 +02:00
|
|
|
type Props = {
|
2020-10-20 21:50:53 +02:00
|
|
|
boardTree: BoardTree
|
2020-10-21 03:28:55 +02:00
|
|
|
cardId: string
|
2020-10-25 13:09:15 +01:00
|
|
|
intl: IntlShape
|
2020-10-15 19:46:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type State = {
|
2020-10-20 21:50:53 +02:00
|
|
|
cardTree?: CardTree
|
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-10-21 18:32:36 +02:00
|
|
|
private cardListener?: OctoListener
|
2020-10-20 21:50:53 +02:00
|
|
|
|
2020-10-25 14:40:47 +01:00
|
|
|
shouldComponentUpdate() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
2020-10-20 21:50:53 +02:00
|
|
|
constructor(props: Props) {
|
2020-10-22 00:03:12 +02:00
|
|
|
super(props)
|
2020-10-25 13:09:15 +01:00
|
|
|
this.state = {}
|
2020-10-20 21:50:53 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
componentDidMount() {
|
|
|
|
this.cardListener = new OctoListener()
|
2020-10-22 00:03:12 +02:00
|
|
|
this.cardListener.open([this.props.cardId], async (blockId) => {
|
2020-10-21 03:28:55 +02:00
|
|
|
Utils.log(`cardListener.onChanged: ${blockId}`)
|
2020-10-20 21:50:53 +02:00
|
|
|
await cardTree.sync()
|
2020-10-22 00:03:12 +02:00
|
|
|
this.setState({...this.state, cardTree})
|
2020-10-20 21:52:56 +02:00
|
|
|
})
|
2020-10-22 00:03:12 +02:00
|
|
|
const cardTree = new MutableCardTree(this.props.cardId)
|
2020-10-20 21:50:53 +02:00
|
|
|
cardTree.sync().then(() => {
|
2020-10-22 00:03:12 +02:00
|
|
|
this.setState({...this.state, cardTree})
|
|
|
|
setTimeout(() => {
|
2020-10-20 21:50:53 +02:00
|
|
|
if (this.titleRef.current) {
|
|
|
|
this.titleRef.current.focus()
|
|
|
|
}
|
2020-10-22 00:03:12 +02:00
|
|
|
}, 0)
|
2020-10-20 21:52:56 +02:00
|
|
|
})
|
2020-10-20 21:50:53 +02:00
|
|
|
}
|
|
|
|
|
2020-10-21 18:32:36 +02:00
|
|
|
componentWillUnmount() {
|
|
|
|
this.cardListener?.close()
|
|
|
|
this.cardListener = undefined
|
|
|
|
}
|
|
|
|
|
2020-10-20 21:50:53 +02:00
|
|
|
render() {
|
2020-10-25 13:09:15 +01:00
|
|
|
const {boardTree, intl} = this.props
|
2020-10-21 03:28:55 +02:00
|
|
|
const {cardTree} = this.state
|
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
|
|
|
|
|
|
|
const newCommentRef = React.createRef<Editable>()
|
|
|
|
const sendCommentButtonRef = React.createRef<HTMLDivElement>()
|
|
|
|
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}
|
|
|
|
cardId={card.id}
|
|
|
|
cardTree={cardTree}
|
|
|
|
/>
|
|
|
|
))}
|
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'>
|
|
|
|
<div className='octo-block octo-hover-container'>
|
|
|
|
<div className='octo-block-margin'/>
|
|
|
|
<MarkdownEditor
|
|
|
|
text=''
|
|
|
|
placeholderText='Add a description...'
|
|
|
|
onChanged={(text) => {
|
2020-10-21 03:28:55 +02:00
|
|
|
const block = new MutableTextBlock()
|
|
|
|
block.parentId = card.id
|
|
|
|
block.title = text
|
|
|
|
block.order = cardTree.contents.length * 1000
|
2020-10-20 21:50:53 +02:00
|
|
|
mutator.insertBlock(block, 'add card text')
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</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'>
|
|
|
|
{icon &&
|
|
|
|
<MenuWrapper>
|
|
|
|
<div className='octo-button octo-icon octo-card-icon'>{icon}</div>
|
|
|
|
<Menu>
|
|
|
|
<Menu.Text
|
|
|
|
id='random'
|
|
|
|
name={intl.formatMessage({id: 'CardDetail.random-icon', defaultMessage: 'Random'})}
|
|
|
|
onClick={() => mutator.changeIcon(card, BlockIcons.shared.randomIcon())}
|
|
|
|
/>
|
|
|
|
<Menu.Text
|
|
|
|
id='remove'
|
|
|
|
name={intl.formatMessage({id: 'CardDetail.remove-icon', defaultMessage: 'Remove Icon'})}
|
|
|
|
onClick={() => mutator.changeIcon(card, undefined, 'remove icon')}
|
|
|
|
/>
|
|
|
|
</Menu>
|
|
|
|
</MenuWrapper>}
|
|
|
|
{!icon &&
|
|
|
|
<div className='octo-hovercontrols'>
|
|
|
|
<Button
|
|
|
|
onClick={() => {
|
|
|
|
const newIcon = BlockIcons.shared.randomIcon()
|
|
|
|
mutator.changeIcon(card, newIcon)
|
|
|
|
}}>
|
|
|
|
<FormattedMessage
|
|
|
|
id='CardDetail.add-icon'
|
|
|
|
defaultMessage='Add Icon'
|
|
|
|
/>
|
|
|
|
</Button>
|
|
|
|
</div>}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
<Editable
|
2020-10-22 00:03:12 +02:00
|
|
|
ref={this.titleRef}
|
|
|
|
className='title'
|
|
|
|
text={card.title}
|
|
|
|
placeholderText='Untitled'
|
|
|
|
onChanged={(text) => {
|
2020-10-20 21:50:53 +02:00
|
|
|
mutator.changeTitle(card, text)
|
|
|
|
}}
|
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) => {
|
2020-10-20 21:50:53 +02:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
key={propertyTemplate.id}
|
|
|
|
className='octo-propertyrow'
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
className='octo-button octo-propertyname'
|
|
|
|
onClick={(e) => {
|
|
|
|
const menu = PropertyMenu.shared
|
2020-10-22 00:03:12 +02:00
|
|
|
menu.property = propertyTemplate
|
2020-10-20 21:50:53 +02:00
|
|
|
menu.onNameChanged = (propertyName) => {
|
2020-10-22 00:03:12 +02:00
|
|
|
Utils.log('menu.onNameChanged')
|
|
|
|
mutator.renameProperty(board, propertyTemplate.id, propertyName)
|
2020-10-20 21:52:56 +02:00
|
|
|
}
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
menu.onMenuClicked = async (command) => {
|
|
|
|
switch (command) {
|
2020-10-22 00:03:12 +02:00
|
|
|
case 'type-text':
|
|
|
|
await mutator.changePropertyType(board, propertyTemplate, 'text')
|
|
|
|
break
|
|
|
|
case 'type-number':
|
2020-10-20 21:50:53 +02:00
|
|
|
await mutator.changePropertyType(board, propertyTemplate, 'number')
|
2020-10-22 00:03:12 +02:00
|
|
|
break
|
2020-10-20 21:50:53 +02:00
|
|
|
case 'type-createdTime':
|
2020-10-22 00:03:12 +02:00
|
|
|
await mutator.changePropertyType(board, propertyTemplate, 'createdTime')
|
2020-10-20 21:52:56 +02:00
|
|
|
break
|
2020-10-22 00:03:12 +02:00
|
|
|
case 'type-updatedTime':
|
2020-10-20 21:50:53 +02:00
|
|
|
await mutator.changePropertyType(board, propertyTemplate, 'updatedTime')
|
2020-10-20 21:52:56 +02:00
|
|
|
break
|
2020-10-22 00:03:12 +02:00
|
|
|
case 'type-select':
|
2020-10-20 21:50:53 +02:00
|
|
|
await mutator.changePropertyType(board, propertyTemplate, 'select')
|
2020-10-20 21:52:56 +02:00
|
|
|
break
|
2020-10-20 21:50:53 +02:00
|
|
|
case 'delete':
|
|
|
|
await mutator.deleteProperty(boardTree, propertyTemplate.id)
|
2020-10-20 21:52:56 +02:00
|
|
|
break
|
2020-10-22 00:03:12 +02:00
|
|
|
default:
|
2020-10-20 21:50:53 +02:00
|
|
|
Utils.assertFailure(`Unhandled menu id: ${command}`)
|
2020-10-22 00:03:12 +02:00
|
|
|
}
|
|
|
|
}
|
2020-10-20 21:50:53 +02:00
|
|
|
menu.showAtElement(e.target as HTMLElement)
|
2020-10-22 00:03:12 +02:00
|
|
|
}}
|
2020-10-20 21:50:53 +02:00
|
|
|
>{propertyTemplate.name}</div>
|
|
|
|
{OctoUtils.propertyValueEditableElement(card, propertyTemplate)}
|
|
|
|
</div>
|
2020-10-22 00:03:12 +02:00
|
|
|
)
|
2020-10-20 21:50:53 +02:00
|
|
|
})}
|
|
|
|
|
2020-10-22 00:03:12 +02:00
|
|
|
<div
|
2020-10-20 21:50:53 +02:00
|
|
|
className='octo-button octo-propertyname'
|
|
|
|
style={{textAlign: 'left', width: '150px', color: 'rgba(55, 53, 37, 0.4)'}}
|
|
|
|
onClick={async () => {
|
|
|
|
// TODO: Show UI
|
2020-10-22 00:03:12 +02:00
|
|
|
await mutator.insertPropertyTemplate(boardTree)
|
|
|
|
}}
|
2020-10-20 21:50:53 +02:00
|
|
|
>+ Add a property</div>
|
2020-10-22 00:03:12 +02:00
|
|
|
</div>
|
2020-10-20 21:50:53 +02:00
|
|
|
|
|
|
|
{/* Comments */}
|
|
|
|
|
|
|
|
<hr/>
|
2020-10-25 15:52:46 +01:00
|
|
|
<CommentsList
|
|
|
|
comments={comments}
|
|
|
|
cardId={card.id}
|
|
|
|
/>
|
2020-10-20 21:50:53 +02:00
|
|
|
<hr/>
|
|
|
|
</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-10-25 13:09:15 +01:00
|
|
|
<div className='CardDetail content'>
|
2020-10-20 21:50:53 +02:00
|
|
|
<div className='octo-hoverpanel octo-hover-container'>
|
2020-10-25 14:40:47 +01:00
|
|
|
<MenuWrapper>
|
|
|
|
<div className='octo-button octo-hovercontrol octo-hover-item'>
|
|
|
|
<FormattedMessage
|
|
|
|
id='CardDetail.add-content'
|
|
|
|
defaultMessage='Add content'
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<Menu>
|
|
|
|
<Menu.Text
|
|
|
|
id='text'
|
|
|
|
name={intl.formatMessage({id: 'CardDetail.text', defaultMessage: 'Text'})}
|
|
|
|
onClick={() => {
|
2020-10-21 03:28:55 +02:00
|
|
|
const block = new MutableTextBlock()
|
|
|
|
block.parentId = card.id
|
|
|
|
block.order = cardTree.contents.length * 1000
|
2020-10-25 14:40:47 +01:00
|
|
|
mutator.insertBlock(block, 'add text')
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<Menu.Text
|
|
|
|
id='image'
|
|
|
|
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
|
|
|
|
onClick={() => Utils.selectLocalFile(
|
|
|
|
(file) => mutator.createImageBlock(card.id, file, cardTree.contents.length * 1000),
|
|
|
|
'.jpg,.jpeg,.png',
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
|
|
|
|
</Menu>
|
|
|
|
</MenuWrapper>
|
2020-10-22 00:03:12 +02:00
|
|
|
</div>
|
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
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
close() {
|
|
|
|
PropertyMenu.shared.hide()
|
|
|
|
}
|
2020-10-15 19:46:32 +02:00
|
|
|
}
|
2020-10-25 13:09:15 +01:00
|
|
|
|
|
|
|
export default injectIntl(CardDetail)
|