Merge pull request #68 from mattermost/content-blocks
Refactor content blocks
This commit is contained in:
commit
4ca343d84c
12 changed files with 353 additions and 147 deletions
|
@ -15,22 +15,14 @@
|
||||||
"CardDetail.add-icon": "Add icon",
|
"CardDetail.add-icon": "Add icon",
|
||||||
"CardDetail.add-property": "+ Add a property",
|
"CardDetail.add-property": "+ Add a property",
|
||||||
"CardDetail.addCardText": "add card text",
|
"CardDetail.addCardText": "add card text",
|
||||||
"CardDetail.image": "Image",
|
|
||||||
"CardDetail.new-comment-placeholder": "Add a comment...",
|
"CardDetail.new-comment-placeholder": "Add a comment...",
|
||||||
"CardDetail.text": "Text",
|
|
||||||
"CardDialog.editing-template": "You're editing a template",
|
"CardDialog.editing-template": "You're editing a template",
|
||||||
"CardDialog.nocard": "This card doesn't exist or is inaccessible",
|
"CardDialog.nocard": "This card doesn't exist or is inaccessible",
|
||||||
"Comment.delete": "Delete",
|
"Comment.delete": "Delete",
|
||||||
"CommentsList.send": "Send",
|
"CommentsList.send": "Send",
|
||||||
"ContentBlock.Delete": "Delete",
|
"ContentBlock.Delete": "Delete",
|
||||||
"ContentBlock.DeleteAction": "delete",
|
"ContentBlock.DeleteAction": "delete",
|
||||||
"ContentBlock.Text": "Text",
|
"ContentBlock.addElement": "add {type}",
|
||||||
"ContentBlock.addDivider": "add divider",
|
|
||||||
"ContentBlock.addImage": "add image",
|
|
||||||
"ContentBlock.addText": "add text",
|
|
||||||
"ContentBlock.divider": "Divider",
|
|
||||||
"ContentBlock.editCardText": "edit card text",
|
|
||||||
"ContentBlock.editText": "Edit text...",
|
|
||||||
"ContentBlock.insertAbove": "Insert above",
|
"ContentBlock.insertAbove": "Insert above",
|
||||||
"ContentBlock.moveDown": "Move down",
|
"ContentBlock.moveDown": "Move down",
|
||||||
"ContentBlock.moveUp": "Move up",
|
"ContentBlock.moveUp": "Move up",
|
||||||
|
@ -98,7 +90,6 @@
|
||||||
"Sidebar.settings": "Settings",
|
"Sidebar.settings": "Settings",
|
||||||
"Sidebar.spanish": "Spanish",
|
"Sidebar.spanish": "Spanish",
|
||||||
"Sidebar.template-from-board": "New template from board",
|
"Sidebar.template-from-board": "New template from board",
|
||||||
"Sidebar.title": "Boards",
|
|
||||||
"Sidebar.untitled": "Untitled",
|
"Sidebar.untitled": "Untitled",
|
||||||
"Sidebar.untitled-board": "(Untitled Board)",
|
"Sidebar.untitled-board": "(Untitled Board)",
|
||||||
"Sidebar.untitled-view": "(Untitled View)",
|
"Sidebar.untitled-view": "(Untitled View)",
|
||||||
|
|
|
@ -2,7 +2,10 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
|
|
||||||
type BlockTypes = 'board' | 'view' | 'card' | 'text' | 'image' | 'divider' | 'comment'
|
const contentBlockTypes = ['text', 'image', 'divider'] as const
|
||||||
|
const blockTypes = [...contentBlockTypes, 'board', 'view', 'card', 'comment'] as const
|
||||||
|
type ContentBlockTypes = typeof contentBlockTypes[number]
|
||||||
|
type BlockTypes = typeof blockTypes[number]
|
||||||
|
|
||||||
interface IBlock {
|
interface IBlock {
|
||||||
readonly id: string
|
readonly id: string
|
||||||
|
@ -69,4 +72,5 @@ class MutableBlock implements IMutableBlock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {IBlock, IMutableBlock, MutableBlock}
|
export type {ContentBlockTypes, BlockTypes}
|
||||||
|
export {blockTypes, contentBlockTypes, IBlock, IMutableBlock, MutableBlock}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import React from 'react'
|
||||||
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
import {BlockIcons} from '../blockIcons'
|
import {BlockIcons} from '../blockIcons'
|
||||||
|
import {BlockTypes} from '../blocks/block'
|
||||||
import {PropertyType} from '../blocks/board'
|
import {PropertyType} from '../blocks/board'
|
||||||
import {MutableTextBlock} from '../blocks/textBlock'
|
import {MutableTextBlock} from '../blocks/textBlock'
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
|
@ -20,6 +21,7 @@ import PropertyMenu from '../widgets/propertyMenu'
|
||||||
import BlockIconSelector from './blockIconSelector'
|
import BlockIconSelector from './blockIconSelector'
|
||||||
import './cardDetail.scss'
|
import './cardDetail.scss'
|
||||||
import CommentsList from './commentsList'
|
import CommentsList from './commentsList'
|
||||||
|
import {ContentHandler, contentRegistry} from './content/contentRegistry'
|
||||||
import ContentBlock from './contentBlock'
|
import ContentBlock from './contentBlock'
|
||||||
import {MarkdownEditor} from './markdownEditor'
|
import {MarkdownEditor} from './markdownEditor'
|
||||||
import PropertyValueElement from './propertyValueElement'
|
import PropertyValueElement from './propertyValueElement'
|
||||||
|
@ -56,7 +58,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {boardTree, cardTree, intl} = this.props
|
const {boardTree, cardTree} = this.props
|
||||||
const {board} = boardTree
|
const {board} = boardTree
|
||||||
if (!cardTree) {
|
if (!cardTree) {
|
||||||
return null
|
return null
|
||||||
|
@ -221,31 +223,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<Menu position='top'>
|
<Menu position='top'>
|
||||||
<Menu.Text
|
{contentRegistry.contentTypes.map((type) => this.addContentMenu(type))}
|
||||||
id='text'
|
|
||||||
name={intl.formatMessage({id: 'CardDetail.text', defaultMessage: 'Text'})}
|
|
||||||
onClick={() => {
|
|
||||||
this.addTextBlock('')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='image'
|
|
||||||
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
|
|
||||||
onClick={() => Utils.selectLocalFile((file) => {
|
|
||||||
mutator.performAsUndoGroup(async () => {
|
|
||||||
const description = intl.formatMessage({id: 'ContentBlock.addImage', defaultMessage: 'add image'})
|
|
||||||
const newBlock = await mutator.createImageBlock(card, file, description)
|
|
||||||
if (newBlock) {
|
|
||||||
const contentOrder = card.contentOrder.slice()
|
|
||||||
contentOrder.push(newBlock.id)
|
|
||||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'.jpg,.jpeg,.png',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
@ -254,6 +232,46 @@ class CardDetail extends React.Component<Props, State> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
key={type}
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private addTextBlock(text: string): void {
|
private addTextBlock(text: string): void {
|
||||||
const {intl, cardTree} = this.props
|
const {intl, cardTree} = this.props
|
||||||
const {card} = cardTree
|
const {card} = cardTree
|
||||||
|
|
38
webapp/src/components/content/contentElement.tsx
Normal file
38
webapp/src/components/content/contentElement.tsx
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import {injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {IContentBlock} from '../../blocks/contentBlock'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
|
import {contentRegistry} from './contentRegistry'
|
||||||
|
|
||||||
|
// Need to require here to prevent webpack from tree-shaking these away
|
||||||
|
// TODO: Update webpack to avoid this
|
||||||
|
import './textElement'
|
||||||
|
import './imageElement'
|
||||||
|
import './dividerElement'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: IContentBlock
|
||||||
|
readonly: boolean
|
||||||
|
intl: IntlShape
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentElement extends React.PureComponent<Props> {
|
||||||
|
public render(): JSX.Element | null {
|
||||||
|
const {block, intl, readonly} = this.props
|
||||||
|
|
||||||
|
const handler = contentRegistry.getHandler(block.type)
|
||||||
|
if (!handler) {
|
||||||
|
Utils.logError(`ContentElement, unknown content type: ${block.type}`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.createComponent(block, intl, readonly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default injectIntl(ContentElement)
|
46
webapp/src/components/content/contentRegistry.tsx
Normal file
46
webapp/src/components/content/contentRegistry.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
/* eslint-disable react/require-optimization */
|
||||||
|
import {IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {BlockTypes} from '../../blocks/block'
|
||||||
|
import {IContentBlock, MutableContentBlock} from '../../blocks/contentBlock'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
|
||||||
|
type ContentHandler = {
|
||||||
|
type: BlockTypes,
|
||||||
|
getDisplayText: (intl: IntlShape) => string,
|
||||||
|
getIcon: () => JSX.Element,
|
||||||
|
createBlock: () => Promise<MutableContentBlock>,
|
||||||
|
createComponent: (block: IContentBlock, intl: IntlShape, readonly: boolean) => JSX.Element,
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentRegistry {
|
||||||
|
private registry: Map<BlockTypes, ContentHandler> = new Map()
|
||||||
|
|
||||||
|
get contentTypes(): BlockTypes[] {
|
||||||
|
return [...this.registry.keys()]
|
||||||
|
}
|
||||||
|
|
||||||
|
registerContentType(entry: ContentHandler) {
|
||||||
|
if (this.isContentType(entry.type)) {
|
||||||
|
Utils.logError(`registerContentType, already registered type: ${entry.type}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.registry.set(entry.type, entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
isContentType(type: BlockTypes): boolean {
|
||||||
|
return this.registry.has(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
getHandler(type: BlockTypes): ContentHandler | undefined {
|
||||||
|
return this.registry.get(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentRegistry = new ContentRegistry()
|
||||||
|
|
||||||
|
export type {ContentHandler}
|
||||||
|
export {contentRegistry}
|
||||||
|
|
6
webapp/src/components/content/dividerElement.scss
Normal file
6
webapp/src/components/content/dividerElement.scss
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
.DividerElement {
|
||||||
|
padding-top: 16px;
|
||||||
|
border-bottom: 1px solid rgba(var(--body-color), 0.09);
|
||||||
|
margin-bottom: 17px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
27
webapp/src/components/content/dividerElement.tsx
Normal file
27
webapp/src/components/content/dividerElement.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
import {MutableDividerBlock} from '../../blocks/dividerBlock'
|
||||||
|
import DividerIcon from '../../widgets/icons/divider'
|
||||||
|
|
||||||
|
import {contentRegistry} from './contentRegistry'
|
||||||
|
import './dividerElement.scss'
|
||||||
|
|
||||||
|
class DividerElement extends React.PureComponent {
|
||||||
|
render(): JSX.Element {
|
||||||
|
return <div className='DividerElement'/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRegistry.registerContentType({
|
||||||
|
type: 'divider',
|
||||||
|
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.divider', defaultMessage: 'divider'}),
|
||||||
|
getIcon: () => <DividerIcon/>,
|
||||||
|
createBlock: async () => {
|
||||||
|
return new MutableDividerBlock()
|
||||||
|
},
|
||||||
|
createComponent: () => <DividerElement/>,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default DividerElement
|
84
webapp/src/components/content/imageElement.tsx
Normal file
84
webapp/src/components/content/imageElement.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
import {injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {IContentBlock} from '../../blocks/contentBlock'
|
||||||
|
import {MutableImageBlock} from '../../blocks/imageBlock'
|
||||||
|
import octoClient from '../../octoClient'
|
||||||
|
import {Utils} from '../../utils'
|
||||||
|
import ImageIcon from '../../widgets/icons/image'
|
||||||
|
|
||||||
|
import {contentRegistry} from './contentRegistry'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: IContentBlock
|
||||||
|
intl: IntlShape
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
imageDataUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class ImageElement extends React.PureComponent<Props> {
|
||||||
|
state: State = {}
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
if (!this.state.imageDataUrl) {
|
||||||
|
this.loadImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadImage() {
|
||||||
|
const imageDataUrl = await octoClient.getFileAsDataUrl(this.props.block.fields.fileId)
|
||||||
|
this.setState({imageDataUrl})
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): JSX.Element | null {
|
||||||
|
const {block} = this.props
|
||||||
|
const {imageDataUrl} = this.state
|
||||||
|
|
||||||
|
if (!imageDataUrl) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={imageDataUrl}
|
||||||
|
alt={block.title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRegistry.registerContentType({
|
||||||
|
type: 'image',
|
||||||
|
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.image', defaultMessage: 'image'}),
|
||||||
|
getIcon: () => <ImageIcon/>,
|
||||||
|
createBlock: async () => {
|
||||||
|
return new Promise<MutableImageBlock>(
|
||||||
|
(resolve) => {
|
||||||
|
Utils.selectLocalFile(async (file) => {
|
||||||
|
const fileId = await octoClient.uploadFile(file)
|
||||||
|
|
||||||
|
const block = new MutableImageBlock()
|
||||||
|
block.fileId = fileId || ''
|
||||||
|
resolve(block)
|
||||||
|
},
|
||||||
|
'.jpg,.jpeg,.png')
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// return new MutableImageBlock()
|
||||||
|
},
|
||||||
|
createComponent: (block, intl) => {
|
||||||
|
return (
|
||||||
|
<ImageElement
|
||||||
|
block={block}
|
||||||
|
intl={intl}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default injectIntl(ImageElement)
|
55
webapp/src/components/content/textElement.tsx
Normal file
55
webapp/src/components/content/textElement.tsx
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
import React from 'react'
|
||||||
|
import {injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {IContentBlock} from '../../blocks/contentBlock'
|
||||||
|
import {MutableTextBlock} from '../../blocks/textBlock'
|
||||||
|
import mutator from '../../mutator'
|
||||||
|
import TextIcon from '../../widgets/icons/text'
|
||||||
|
import {MarkdownEditor} from '../markdownEditor'
|
||||||
|
|
||||||
|
import {contentRegistry} from './contentRegistry'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
block: IContentBlock
|
||||||
|
readonly: boolean
|
||||||
|
intl: IntlShape
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextElement extends React.PureComponent<Props> {
|
||||||
|
render(): JSX.Element {
|
||||||
|
const {intl, block, readonly} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MarkdownEditor
|
||||||
|
text={block.title}
|
||||||
|
placeholderText={intl.formatMessage({id: 'ContentBlock.editText', defaultMessage: 'Edit text...'})}
|
||||||
|
onBlur={(text) => {
|
||||||
|
mutator.changeTitle(block, text, intl.formatMessage({id: 'ContentBlock.editCardText', defaultMessage: 'edit card text'}))
|
||||||
|
}}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentRegistry.registerContentType({
|
||||||
|
type: 'text',
|
||||||
|
getDisplayText: (intl) => intl.formatMessage({id: 'ContentBlock.text', defaultMessage: 'text'}),
|
||||||
|
getIcon: () => <TextIcon/>,
|
||||||
|
createBlock: async () => {
|
||||||
|
return new MutableTextBlock()
|
||||||
|
},
|
||||||
|
createComponent: (block, intl, readonly) => {
|
||||||
|
return (
|
||||||
|
<TextElement
|
||||||
|
block={block}
|
||||||
|
intl={intl}
|
||||||
|
readonly={readonly}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default injectIntl(TextElement)
|
|
@ -7,12 +7,6 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.divider {
|
|
||||||
padding-top: 16px;
|
|
||||||
border-bottom: 1px solid rgba(var(--body-color), 0.09);
|
|
||||||
margin-bottom: 17px;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
> * {
|
> * {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,27 +4,23 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {injectIntl, IntlShape} from 'react-intl'
|
import {injectIntl, IntlShape} from 'react-intl'
|
||||||
|
|
||||||
|
import {BlockTypes} from '../blocks/block'
|
||||||
import {Card} from '../blocks/card'
|
import {Card} from '../blocks/card'
|
||||||
import {IContentBlock} from '../blocks/contentBlock'
|
import {IContentBlock} from '../blocks/contentBlock'
|
||||||
import {MutableDividerBlock} from '../blocks/dividerBlock'
|
|
||||||
import {MutableTextBlock} from '../blocks/textBlock'
|
|
||||||
import mutator from '../mutator'
|
import mutator from '../mutator'
|
||||||
import octoClient from '../octoClient'
|
|
||||||
import {Utils} from '../utils'
|
import {Utils} from '../utils'
|
||||||
import IconButton from '../widgets/buttons/iconButton'
|
import IconButton from '../widgets/buttons/iconButton'
|
||||||
import AddIcon from '../widgets/icons/add'
|
import AddIcon from '../widgets/icons/add'
|
||||||
import DeleteIcon from '../widgets/icons/delete'
|
import DeleteIcon from '../widgets/icons/delete'
|
||||||
import DividerIcon from '../widgets/icons/divider'
|
|
||||||
import ImageIcon from '../widgets/icons/image'
|
|
||||||
import OptionsIcon from '../widgets/icons/options'
|
import OptionsIcon from '../widgets/icons/options'
|
||||||
import SortDownIcon from '../widgets/icons/sortDown'
|
import SortDownIcon from '../widgets/icons/sortDown'
|
||||||
import SortUpIcon from '../widgets/icons/sortUp'
|
import SortUpIcon from '../widgets/icons/sortUp'
|
||||||
import TextIcon from '../widgets/icons/text'
|
|
||||||
import Menu from '../widgets/menu'
|
import Menu from '../widgets/menu'
|
||||||
import MenuWrapper from '../widgets/menuWrapper'
|
import MenuWrapper from '../widgets/menuWrapper'
|
||||||
|
|
||||||
|
import ContentElement from './content/contentElement'
|
||||||
|
import {contentRegistry} from './content/contentRegistry'
|
||||||
import './contentBlock.scss'
|
import './contentBlock.scss'
|
||||||
import {MarkdownEditor} from './markdownEditor'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
block: IContentBlock
|
block: IContentBlock
|
||||||
|
@ -34,31 +30,9 @@ type Props = {
|
||||||
intl: IntlShape
|
intl: IntlShape
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
class ContentBlock extends React.PureComponent<Props> {
|
||||||
imageDataUrl?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContentBlock extends React.PureComponent<Props, State> {
|
|
||||||
state: State = {}
|
|
||||||
|
|
||||||
componentDidMount(): void {
|
|
||||||
if (this.props.block.type === 'image' && !this.state.imageDataUrl) {
|
|
||||||
this.loadImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadImage() {
|
|
||||||
const imageDataUrl = await octoClient.getFileAsDataUrl(this.props.block.fields.fileId)
|
|
||||||
this.setState({imageDataUrl})
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): JSX.Element | null {
|
public render(): JSX.Element | null {
|
||||||
const {intl, card, contents, block} = this.props
|
const {intl, card, contents, block, readonly} = this.props
|
||||||
|
|
||||||
if (block.type !== 'text' && block.type !== 'image' && block.type !== 'divider') {
|
|
||||||
Utils.assertFailure(`Block type is unknown: ${block.type}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = contents.indexOf(block)
|
const index = contents.indexOf(block)
|
||||||
return (
|
return (
|
||||||
|
@ -95,61 +69,7 @@ class ContentBlock extends React.PureComponent<Props, State> {
|
||||||
name={intl.formatMessage({id: 'ContentBlock.insertAbove', defaultMessage: 'Insert above'})}
|
name={intl.formatMessage({id: 'ContentBlock.insertAbove', defaultMessage: 'Insert above'})}
|
||||||
icon={<AddIcon/>}
|
icon={<AddIcon/>}
|
||||||
>
|
>
|
||||||
<Menu.Text
|
{contentRegistry.contentTypes.map((type) => this.addContentMenu(type))}
|
||||||
id='text'
|
|
||||||
name={intl.formatMessage({id: 'ContentBlock.Text', defaultMessage: 'Text'})}
|
|
||||||
icon={<TextIcon/>}
|
|
||||||
onClick={() => {
|
|
||||||
const newBlock = new MutableTextBlock()
|
|
||||||
newBlock.parentId = card.id
|
|
||||||
newBlock.rootId = card.rootId
|
|
||||||
|
|
||||||
const contentOrder = contents.map((o) => o.id)
|
|
||||||
contentOrder.splice(index, 0, newBlock.id)
|
|
||||||
mutator.performAsUndoGroup(async () => {
|
|
||||||
const description = intl.formatMessage({id: 'ContentBlock.addText', defaultMessage: 'add text'})
|
|
||||||
await mutator.insertBlock(newBlock, description)
|
|
||||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='image'
|
|
||||||
name='Image'
|
|
||||||
icon={<ImageIcon/>}
|
|
||||||
onClick={() => {
|
|
||||||
Utils.selectLocalFile((file) => {
|
|
||||||
mutator.performAsUndoGroup(async () => {
|
|
||||||
const description = intl.formatMessage({id: 'ContentBlock.addImage', defaultMessage: 'add image'})
|
|
||||||
const newBlock = await mutator.createImageBlock(card, file, description)
|
|
||||||
if (newBlock) {
|
|
||||||
const contentOrder = contents.map((o) => o.id)
|
|
||||||
contentOrder.splice(index, 0, newBlock.id)
|
|
||||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
'.jpg,.jpeg,.png')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Menu.Text
|
|
||||||
id='divider'
|
|
||||||
name={intl.formatMessage({id: 'ContentBlock.divider', defaultMessage: 'Divider'})}
|
|
||||||
icon={<DividerIcon/>}
|
|
||||||
onClick={() => {
|
|
||||||
const newBlock = new MutableDividerBlock()
|
|
||||||
newBlock.parentId = card.id
|
|
||||||
newBlock.rootId = card.rootId
|
|
||||||
|
|
||||||
const contentOrder = contents.map((o) => o.id)
|
|
||||||
contentOrder.splice(index, 0, newBlock.id)
|
|
||||||
mutator.performAsUndoGroup(async () => {
|
|
||||||
const description = intl.formatMessage({id: 'ContentBlock.addDivider', defaultMessage: 'add divider'})
|
|
||||||
await mutator.insertBlock(newBlock, description)
|
|
||||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Menu.SubMenu>
|
</Menu.SubMenu>
|
||||||
<Menu.Text
|
<Menu.Text
|
||||||
icon={<DeleteIcon/>}
|
icon={<DeleteIcon/>}
|
||||||
|
@ -168,24 +88,47 @@ class ContentBlock extends React.PureComponent<Props, State> {
|
||||||
</MenuWrapper>
|
</MenuWrapper>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
{block.type === 'text' &&
|
<ContentElement
|
||||||
<MarkdownEditor
|
block={block}
|
||||||
text={block.title}
|
readonly={readonly}
|
||||||
placeholderText={intl.formatMessage({id: 'ContentBlock.editText', defaultMessage: 'Edit text...'})}
|
/>
|
||||||
onBlur={(text) => {
|
|
||||||
mutator.changeTitle(block, text, intl.formatMessage({id: 'ContentBlock.editCardText', defaultMessage: 'edit card text'}))
|
|
||||||
}}
|
|
||||||
readonly={this.props.readonly}
|
|
||||||
/>}
|
|
||||||
{block.type === 'divider' && <div className='divider'/>}
|
|
||||||
{block.type === 'image' && this.state.imageDataUrl &&
|
|
||||||
<img
|
|
||||||
src={this.state.imageDataUrl}
|
|
||||||
alt={block.title}
|
|
||||||
/>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private addContentMenu(type: BlockTypes): JSX.Element {
|
||||||
|
const {intl, card, contents, block} = this.props
|
||||||
|
const index = contents.indexOf(block)
|
||||||
|
|
||||||
|
const handler = contentRegistry.getHandler(type)
|
||||||
|
if (!handler) {
|
||||||
|
Utils.logError(`addContentMenu, unknown content type: ${type}`)
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Text
|
||||||
|
key={type}
|
||||||
|
id={type}
|
||||||
|
name={handler.getDisplayText(intl)}
|
||||||
|
icon={handler.getIcon()}
|
||||||
|
onClick={async () => {
|
||||||
|
const newBlock = await handler.createBlock()
|
||||||
|
newBlock.parentId = card.id
|
||||||
|
newBlock.rootId = card.rootId
|
||||||
|
|
||||||
|
const contentOrder = contents.map((o) => o.id)
|
||||||
|
contentOrder.splice(index, 0, 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)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default injectIntl(ContentBlock)
|
export default injectIntl(ContentBlock)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {IBlock} from '../blocks/block'
|
import {ContentBlockTypes, contentBlockTypes, IBlock} from '../blocks/block'
|
||||||
import {Card, MutableCard} from '../blocks/card'
|
import {Card, MutableCard} from '../blocks/card'
|
||||||
import {CommentBlock} from '../blocks/commentBlock'
|
import {CommentBlock} from '../blocks/commentBlock'
|
||||||
import {IContentBlock} from '../blocks/contentBlock'
|
import {IContentBlock} from '../blocks/contentBlock'
|
||||||
|
@ -56,7 +56,7 @@ class MutableCardTree implements CardTree {
|
||||||
filter((block) => block.type === 'comment').
|
filter((block) => block.type === 'comment').
|
||||||
sort((a, b) => a.createAt - b.createAt) as CommentBlock[]
|
sort((a, b) => a.createAt - b.createAt) as CommentBlock[]
|
||||||
|
|
||||||
const contentBlocks = blocks.filter((block) => block.type === 'text' || block.type === 'image' || block.type === 'divider') as IContentBlock[]
|
const contentBlocks = blocks.filter((block) => contentBlockTypes.includes(block.type as ContentBlockTypes)) as IContentBlock[]
|
||||||
cardTree.contents = OctoUtils.getBlockOrder(card.contentOrder, contentBlocks)
|
cardTree.contents = OctoUtils.getBlockOrder(card.contentOrder, contentBlocks)
|
||||||
|
|
||||||
return cardTree
|
return cardTree
|
||||||
|
|
Loading…
Reference in a new issue