focalboard/webapp/src/components/cardDetail.tsx

343 lines
15 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'
import {FormattedMessage, IntlShape, injectIntl} from 'react-intl'
2020-10-21 03:28:55 +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
import MenuWrapper from '../widgets/menuWrapper'
import Menu from '../widgets/menu'
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-24 20:37:09 +02:00
import Comment from './comment'
import './cardDetail.scss'
type Props = {
2020-10-20 21:50:53 +02:00
boardTree: BoardTree
2020-10-21 03:28:55 +02:00
cardId: string
intl: IntlShape
}
type State = {
2020-10-20 21:50:53 +02:00
cardTree?: CardTree
}
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) {
super(props)
this.state = {}
2020-10-20 21:50:53 +02:00
}
componentDidMount() {
this.cardListener = new OctoListener()
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()
this.setState({...this.state, cardTree})
2020-10-20 21:52:56 +02:00
})
const cardTree = new MutableCardTree(this.props.cardId)
2020-10-20 21:50:53 +02:00
cardTree.sync().then(() => {
this.setState({...this.state, cardTree})
setTimeout(() => {
2020-10-20 21:50:53 +02:00
if (this.titleRef.current) {
this.titleRef.current.focus()
}
}, 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() {
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
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>)
} 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-20 21:50:53 +02:00
const icon = card.icon
// TODO: Replace this placeholder
2020-10-20 21:52:56 +02:00
const username = 'John Smith'
const userImageUrl = 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" style="fill: rgb(192, 192, 192);"><rect width="100" height="100" /></svg>'
2020-10-20 21:50:53 +02:00
return (
2020-10-20 21:50:53 +02: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
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-20 21:50:53 +02:00
{/* Property list */}
<div className='octo-propertylist'>
{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
menu.property = propertyTemplate
2020-10-20 21:50:53 +02:00
menu.onNameChanged = (propertyName) => {
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) {
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')
break
2020-10-20 21:50:53 +02:00
case 'type-createdTime':
await mutator.changePropertyType(board, propertyTemplate, 'createdTime')
2020-10-20 21:52:56 +02:00
break
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
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
default:
2020-10-20 21:50:53 +02:00
Utils.assertFailure(`Unhandled menu id: ${command}`)
}
}
2020-10-20 21:50:53 +02:00
menu.showAtElement(e.target as HTMLElement)
}}
2020-10-20 21:50:53 +02:00
>{propertyTemplate.name}</div>
{OctoUtils.propertyValueEditableElement(card, propertyTemplate)}
</div>
)
2020-10-20 21:50:53 +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
await mutator.insertPropertyTemplate(boardTree)
}}
2020-10-20 21:50:53 +02:00
>+ Add a property</div>
</div>
2020-10-20 21:50:53 +02:00
{/* Comments */}
<hr/>
<div className='commentlist'>
2020-10-24 20:37:09 +02:00
{comments.map((comment) => (
<Comment
2020-10-20 21:50:53 +02:00
key={comment.id}
2020-10-24 20:37:09 +02:00
comment={comment}
userImageUrl={userImageUrl}
username={username}
/>
))}
2020-10-20 21:50:53 +02:00
{/* New comment */}
2020-10-20 21:50:53 +02:00
<div className='commentrow'>
2020-10-20 21:50:53 +02:00
<img
className='comment-avatar'
src={userImageUrl}
/>
2020-10-20 21:50:53 +02:00
<Editable
ref={newCommentRef}
className='newcomment'
placeholderText={intl.formatMessage({id: 'CardDetail.new-comment-placeholder', defaultMessage: 'Add a comment...'})}
onChanged={(text) => { }}
onFocus={() => {
2020-10-20 21:50:53 +02:00
sendCommentButtonRef.current.style.display = null
}}
onBlur={() => {
if (!newCommentRef.current.text) {
2020-10-20 21:52:56 +02:00
sendCommentButtonRef.current.style.display = 'none'
}
}}
onKeyDown={(e) => {
2020-10-20 21:50:53 +02:00
if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
sendCommentButtonRef.current.click()
2020-10-20 21:50:53 +02:00
}
}}
/>
2020-10-20 21:50:53 +02:00
<div
ref={sendCommentButtonRef}
className='octo-button filled'
style={{display: 'none'}}
onClick={(e) => {
const text = newCommentRef.current.text
2020-10-22 18:46:06 +02:00
Utils.log(`Send comment: ${newCommentRef.current.text}`)
this.sendComment(text)
newCommentRef.current.text = undefined
2020-10-20 21:50:53 +02:00
newCommentRef.current.blur()
}}
>Send</div>
2020-10-20 21:50:53 +02:00
</div>
</div>
2020-10-20 21:50:53 +02:00
<hr/>
</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>
<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>
</div>
2020-10-20 21:50:53 +02:00
</div>
</>
2020-10-20 21:50:53 +02:00
)
}
async sendComment(text: string) {
2020-10-21 03:28:55 +02:00
const {cardId} = this.props
2020-10-20 21:50:53 +02:00
2020-10-21 03:28:55 +02:00
Utils.assertValue(cardId)
2020-10-20 21:50:53 +02:00
const block = new MutableCommentBlock({parentId: cardId, title: text})
2020-10-20 21:50:53 +02:00
await mutator.insertBlock(block, 'add comment')
}
close() {
PropertyMenu.shared.hide()
}
}
export default injectIntl(CardDetail)