mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-15 09:14:11 +02:00
Merge pull request #68 from mattermost/content-blocks
Refactor content blocks
This commit is contained in:
commit
4ca343d84c
@ -15,22 +15,14 @@
|
||||
"CardDetail.add-icon": "Add icon",
|
||||
"CardDetail.add-property": "+ Add a property",
|
||||
"CardDetail.addCardText": "add card text",
|
||||
"CardDetail.image": "Image",
|
||||
"CardDetail.new-comment-placeholder": "Add a comment...",
|
||||
"CardDetail.text": "Text",
|
||||
"CardDialog.editing-template": "You're editing a template",
|
||||
"CardDialog.nocard": "This card doesn't exist or is inaccessible",
|
||||
"Comment.delete": "Delete",
|
||||
"CommentsList.send": "Send",
|
||||
"ContentBlock.Delete": "Delete",
|
||||
"ContentBlock.DeleteAction": "delete",
|
||||
"ContentBlock.Text": "Text",
|
||||
"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.addElement": "add {type}",
|
||||
"ContentBlock.insertAbove": "Insert above",
|
||||
"ContentBlock.moveDown": "Move down",
|
||||
"ContentBlock.moveUp": "Move up",
|
||||
@ -98,7 +90,6 @@
|
||||
"Sidebar.settings": "Settings",
|
||||
"Sidebar.spanish": "Spanish",
|
||||
"Sidebar.template-from-board": "New template from board",
|
||||
"Sidebar.title": "Boards",
|
||||
"Sidebar.untitled": "Untitled",
|
||||
"Sidebar.untitled-board": "(Untitled Board)",
|
||||
"Sidebar.untitled-view": "(Untitled View)",
|
||||
|
@ -2,7 +2,10 @@
|
||||
// See LICENSE.txt for license information.
|
||||
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 {
|
||||
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 {BlockIcons} from '../blockIcons'
|
||||
import {BlockTypes} from '../blocks/block'
|
||||
import {PropertyType} from '../blocks/board'
|
||||
import {MutableTextBlock} from '../blocks/textBlock'
|
||||
import mutator from '../mutator'
|
||||
@ -20,6 +21,7 @@ import PropertyMenu from '../widgets/propertyMenu'
|
||||
import BlockIconSelector from './blockIconSelector'
|
||||
import './cardDetail.scss'
|
||||
import CommentsList from './commentsList'
|
||||
import {ContentHandler, contentRegistry} from './content/contentRegistry'
|
||||
import ContentBlock from './contentBlock'
|
||||
import {MarkdownEditor} from './markdownEditor'
|
||||
import PropertyValueElement from './propertyValueElement'
|
||||
@ -56,7 +58,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {boardTree, cardTree, intl} = this.props
|
||||
const {boardTree, cardTree} = this.props
|
||||
const {board} = boardTree
|
||||
if (!cardTree) {
|
||||
return null
|
||||
@ -221,31 +223,7 @@ class CardDetail extends React.Component<Props, State> {
|
||||
/>
|
||||
</Button>
|
||||
<Menu position='top'>
|
||||
<Menu.Text
|
||||
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',
|
||||
)}
|
||||
/>
|
||||
|
||||
{contentRegistry.contentTypes.map((type) => this.addContentMenu(type))}
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</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 {
|
||||
const {intl, cardTree} = this.props
|
||||
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;
|
||||
}
|
||||
}
|
||||
.divider {
|
||||
padding-top: 16px;
|
||||
border-bottom: 1px solid rgba(var(--body-color), 0.09);
|
||||
margin-bottom: 17px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
> * {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
@ -4,27 +4,23 @@
|
||||
import React from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {BlockTypes} from '../blocks/block'
|
||||
import {Card} from '../blocks/card'
|
||||
import {IContentBlock} from '../blocks/contentBlock'
|
||||
import {MutableDividerBlock} from '../blocks/dividerBlock'
|
||||
import {MutableTextBlock} from '../blocks/textBlock'
|
||||
import mutator from '../mutator'
|
||||
import octoClient from '../octoClient'
|
||||
import {Utils} from '../utils'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
import AddIcon from '../widgets/icons/add'
|
||||
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 SortDownIcon from '../widgets/icons/sortDown'
|
||||
import SortUpIcon from '../widgets/icons/sortUp'
|
||||
import TextIcon from '../widgets/icons/text'
|
||||
import Menu from '../widgets/menu'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
|
||||
import ContentElement from './content/contentElement'
|
||||
import {contentRegistry} from './content/contentRegistry'
|
||||
import './contentBlock.scss'
|
||||
import {MarkdownEditor} from './markdownEditor'
|
||||
|
||||
type Props = {
|
||||
block: IContentBlock
|
||||
@ -34,31 +30,9 @@ type Props = {
|
||||
intl: IntlShape
|
||||
}
|
||||
|
||||
type State = {
|
||||
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})
|
||||
}
|
||||
|
||||
class ContentBlock extends React.PureComponent<Props> {
|
||||
public render(): JSX.Element | null {
|
||||
const {intl, card, contents, block} = this.props
|
||||
|
||||
if (block.type !== 'text' && block.type !== 'image' && block.type !== 'divider') {
|
||||
Utils.assertFailure(`Block type is unknown: ${block.type}`)
|
||||
return null
|
||||
}
|
||||
const {intl, card, contents, block, readonly} = this.props
|
||||
|
||||
const index = contents.indexOf(block)
|
||||
return (
|
||||
@ -95,61 +69,7 @@ class ContentBlock extends React.PureComponent<Props, State> {
|
||||
name={intl.formatMessage({id: 'ContentBlock.insertAbove', defaultMessage: 'Insert above'})}
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
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)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
{contentRegistry.contentTypes.map((type) => this.addContentMenu(type))}
|
||||
</Menu.SubMenu>
|
||||
<Menu.Text
|
||||
icon={<DeleteIcon/>}
|
||||
@ -168,24 +88,47 @@ class ContentBlock extends React.PureComponent<Props, State> {
|
||||
</MenuWrapper>
|
||||
}
|
||||
</div>
|
||||
{block.type === 'text' &&
|
||||
<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={this.props.readonly}
|
||||
/>}
|
||||
{block.type === 'divider' && <div className='divider'/>}
|
||||
{block.type === 'image' && this.state.imageDataUrl &&
|
||||
<img
|
||||
src={this.state.imageDataUrl}
|
||||
alt={block.title}
|
||||
/>}
|
||||
<ContentElement
|
||||
block={block}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</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)
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 {CommentBlock} from '../blocks/commentBlock'
|
||||
import {IContentBlock} from '../blocks/contentBlock'
|
||||
@ -56,7 +56,7 @@ class MutableCardTree implements CardTree {
|
||||
filter((block) => block.type === 'comment').
|
||||
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)
|
||||
|
||||
return cardTree
|
||||
|
Loading…
Reference in New Issue
Block a user