Refactor: card contentOrder
This commit is contained in:
parent
f22527e650
commit
68f5130098
17 changed files with 186 additions and 126 deletions
|
@ -58,7 +58,8 @@
|
|||
}
|
||||
],
|
||||
"react/no-string-refs": 2,
|
||||
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}]
|
||||
"no-only-tests/no-only-tests": ["error", {"focus": ["only", "skip"]}],
|
||||
"max-nested-callbacks": ["error", {"max": 5}]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"CardDetail.add-content": "Add content",
|
||||
"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",
|
||||
|
@ -20,6 +21,18 @@
|
|||
"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.insertAbove": "Insert above",
|
||||
"ContentBlock.moveDown": "Move down",
|
||||
"ContentBlock.moveUp": "Move up",
|
||||
"Filter.includes": "includes",
|
||||
"Filter.is-empty": "is empty",
|
||||
"Filter.is-not-empty": "is not empty",
|
||||
|
|
|
@ -63,7 +63,6 @@ test('block: clone text', async () => {
|
|||
const blockB = new MutableTextBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
expect(blockB.order).toEqual(blockA.order)
|
||||
})
|
||||
|
||||
test('block: clone image', async () => {
|
||||
|
@ -72,7 +71,6 @@ test('block: clone image', async () => {
|
|||
const blockB = new MutableImageBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
expect(blockB.order).toEqual(blockA.order)
|
||||
expect(blockB.url.length).toBeGreaterThan(0)
|
||||
expect(blockB.url).toEqual(blockA.url)
|
||||
})
|
||||
|
@ -83,5 +81,4 @@ test('block: clone divider', async () => {
|
|||
const blockB = new MutableDividerBlock(blockA)
|
||||
|
||||
expect(blockB).toEqual(blockA)
|
||||
expect(blockB.order).toEqual(blockA.order)
|
||||
})
|
||||
|
|
|
@ -9,6 +9,8 @@ interface Card extends IBlock {
|
|||
readonly icon: string
|
||||
readonly isTemplate: boolean
|
||||
readonly properties: Readonly<Record<string, string>>
|
||||
readonly contentOrder: readonly string[]
|
||||
|
||||
duplicate(): MutableCard
|
||||
}
|
||||
|
||||
|
@ -34,12 +36,20 @@ class MutableCard extends MutableBlock {
|
|||
this.fields.properties = value
|
||||
}
|
||||
|
||||
get contentOrder(): string[] {
|
||||
return this.fields.contentOrder
|
||||
}
|
||||
set contentOrder(value: string[]) {
|
||||
this.fields.contentOrder = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'card'
|
||||
|
||||
this.icon = block.fields?.icon || ''
|
||||
this.properties = {...(block.fields?.properties || {})}
|
||||
this.contentOrder = block.fields?.contentOrder?.slice() || []
|
||||
}
|
||||
|
||||
duplicate(): MutableCard {
|
||||
|
|
10
webapp/src/blocks/contentBlock.ts
Normal file
10
webapp/src/blocks/contentBlock.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock, MutableBlock} from './block'
|
||||
|
||||
type IContentBlock = IBlock
|
||||
|
||||
class MutableContentBlock extends MutableBlock implements IContentBlock {
|
||||
}
|
||||
|
||||
export {IContentBlock, MutableContentBlock}
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './orderedBlock'
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
|
||||
type DividerBlock = IOrderedBlock
|
||||
type DividerBlock = IContentBlock
|
||||
|
||||
class MutableDividerBlock extends MutableOrderedBlock {
|
||||
class MutableDividerBlock extends MutableContentBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'divider'
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './orderedBlock'
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
|
||||
interface ImageBlock extends IOrderedBlock {
|
||||
interface ImageBlock extends IContentBlock {
|
||||
readonly url: string
|
||||
}
|
||||
|
||||
class MutableImageBlock extends MutableOrderedBlock implements IOrderedBlock {
|
||||
class MutableImageBlock extends MutableContentBlock implements IContentBlock {
|
||||
get url(): string {
|
||||
return this.fields.url as string
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IBlock} from '../blocks/block'
|
||||
|
||||
import {MutableBlock} from './block'
|
||||
|
||||
interface IOrderedBlock extends IBlock {
|
||||
readonly order: number
|
||||
}
|
||||
|
||||
class MutableOrderedBlock extends MutableBlock implements IOrderedBlock {
|
||||
get order(): number {
|
||||
return this.fields.order as number
|
||||
}
|
||||
set order(value: number) {
|
||||
this.fields.order = value
|
||||
}
|
||||
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.order = block.fields?.order || 0
|
||||
}
|
||||
}
|
||||
|
||||
export {IOrderedBlock, MutableOrderedBlock}
|
|
@ -1,10 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './orderedBlock'
|
||||
import {IContentBlock, MutableContentBlock} from './contentBlock'
|
||||
|
||||
type TextBlock = IOrderedBlock
|
||||
type TextBlock = IContentBlock
|
||||
|
||||
class MutableTextBlock extends MutableOrderedBlock {
|
||||
class MutableTextBlock extends MutableContentBlock {
|
||||
constructor(block: any = {}) {
|
||||
super(block)
|
||||
this.type = 'text'
|
||||
|
|
|
@ -89,12 +89,7 @@ class CardDetail extends React.Component<Props, State> {
|
|||
placeholderText='Add a description...'
|
||||
onBlur={(text) => {
|
||||
if (text) {
|
||||
const block = new MutableTextBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.title = text
|
||||
block.order = (this.props.cardTree.contents.length + 1) * 1000
|
||||
mutator.insertBlock(block, 'add card text')
|
||||
this.addTextBlock(text)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@ -230,19 +225,24 @@ class CardDetail extends React.Component<Props, State> {
|
|||
id='text'
|
||||
name={intl.formatMessage({id: 'CardDetail.text', defaultMessage: 'Text'})}
|
||||
onClick={() => {
|
||||
const block = new MutableTextBlock()
|
||||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.order = (this.props.cardTree.contents.length + 1) * 1000
|
||||
mutator.insertBlock(block, 'add text')
|
||||
this.addTextBlock('')
|
||||
}}
|
||||
/>
|
||||
<Menu.Text
|
||||
id='image'
|
||||
name={intl.formatMessage({id: 'CardDetail.image', defaultMessage: 'Image'})}
|
||||
onClick={() => Utils.selectLocalFile(
|
||||
(file) => mutator.createImageBlock(card, file, (this.props.cardTree.contents.length + 1) * 1000),
|
||||
'.jpg,.jpeg,.png',
|
||||
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',
|
||||
)}
|
||||
/>
|
||||
|
||||
|
@ -253,6 +253,24 @@ class CardDetail extends React.Component<Props, State> {
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(CardDetail)
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react'
|
||||
import {injectIntl, IntlShape} from 'react-intl'
|
||||
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {Card} from '../blocks/card'
|
||||
import {IContentBlock} from '../blocks/contentBlock'
|
||||
import {MutableDividerBlock} from '../blocks/dividerBlock'
|
||||
import {IOrderedBlock} from '../blocks/orderedBlock'
|
||||
import {MutableTextBlock} from '../blocks/textBlock'
|
||||
import mutator from '../mutator'
|
||||
import {OctoUtils} from '../octoUtils'
|
||||
import {Utils} from '../utils'
|
||||
import IconButton from '../widgets/buttons/iconButton'
|
||||
import AddIcon from '../widgets/icons/add'
|
||||
|
@ -26,15 +26,16 @@ import './contentBlock.scss'
|
|||
import {MarkdownEditor} from './markdownEditor'
|
||||
|
||||
type Props = {
|
||||
block: IOrderedBlock
|
||||
card: IBlock
|
||||
contents: readonly IOrderedBlock[]
|
||||
block: IContentBlock
|
||||
card: Card
|
||||
contents: readonly IContentBlock[]
|
||||
readonly: boolean
|
||||
intl: IntlShape
|
||||
}
|
||||
|
||||
class ContentBlock extends React.PureComponent<Props> {
|
||||
public render(): JSX.Element | null {
|
||||
const {card, contents, block} = this.props
|
||||
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}`)
|
||||
|
@ -52,45 +53,46 @@ class ContentBlock extends React.PureComponent<Props> {
|
|||
{index > 0 &&
|
||||
<Menu.Text
|
||||
id='moveUp'
|
||||
name='Move up'
|
||||
name={intl.formatMessage({id: 'ContentBlock.moveUp', defaultMessage: 'Move up'})}
|
||||
icon={<SortUpIcon/>}
|
||||
onClick={() => {
|
||||
const previousBlock = contents[index - 1]
|
||||
const newOrder = OctoUtils.getOrderBefore(previousBlock, contents)
|
||||
Utils.log(`moveUp ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move up')
|
||||
const contentOrder = contents.map((o) => o.id)
|
||||
Utils.arrayMove(contentOrder, index, index - 1)
|
||||
mutator.changeCardContentOrder(card, contentOrder)
|
||||
}}
|
||||
/>}
|
||||
{index < (contents.length - 1) &&
|
||||
<Menu.Text
|
||||
id='moveDown'
|
||||
name='Move down'
|
||||
name={intl.formatMessage({id: 'ContentBlock.moveDown', defaultMessage: 'Move down'})}
|
||||
icon={<SortDownIcon/>}
|
||||
onClick={() => {
|
||||
const nextBlock = contents[index + 1]
|
||||
const newOrder = OctoUtils.getOrderAfter(nextBlock, contents)
|
||||
Utils.log(`moveDown ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move down')
|
||||
const contentOrder = contents.map((o) => o.id)
|
||||
Utils.arrayMove(contentOrder, index, index + 1)
|
||||
mutator.changeCardContentOrder(card, contentOrder)
|
||||
}}
|
||||
/>}
|
||||
<Menu.SubMenu
|
||||
id='insertAbove'
|
||||
name='Insert above'
|
||||
name={intl.formatMessage({id: 'ContentBlock.insertAbove', defaultMessage: 'Insert above'})}
|
||||
icon={<AddIcon/>}
|
||||
>
|
||||
<Menu.Text
|
||||
id='text'
|
||||
name='Text'
|
||||
name={intl.formatMessage({id: 'ContentBlock.Text', defaultMessage: 'Text'})}
|
||||
icon={<TextIcon/>}
|
||||
onClick={() => {
|
||||
const newBlock = new MutableTextBlock()
|
||||
newBlock.parentId = card.id
|
||||
newBlock.rootId = card.rootId
|
||||
|
||||
// TODO: Handle need to reorder all blocks
|
||||
newBlock.order = OctoUtils.getOrderBefore(block, contents)
|
||||
Utils.log(`insert block ${block.id}, order: ${block.order}`)
|
||||
mutator.insertBlock(newBlock, 'insert card text')
|
||||
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
|
||||
|
@ -98,34 +100,51 @@ class ContentBlock extends React.PureComponent<Props> {
|
|||
name='Image'
|
||||
icon={<ImageIcon/>}
|
||||
onClick={() => {
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(card, file, OctoUtils.getOrderBefore(block, contents))
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
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='Divider'
|
||||
name={intl.formatMessage({id: 'ContentBlock.divider', defaultMessage: 'Divider'})}
|
||||
icon={<DividerIcon/>}
|
||||
onClick={() => {
|
||||
const newBlock = new MutableDividerBlock()
|
||||
newBlock.parentId = card.id
|
||||
newBlock.rootId = card.rootId
|
||||
|
||||
// TODO: Handle need to reorder all blocks
|
||||
newBlock.order = OctoUtils.getOrderBefore(block, contents)
|
||||
Utils.log(`insert block ${block.id}, order: ${block.order}`)
|
||||
mutator.insertBlock(newBlock, 'insert card text')
|
||||
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.Text
|
||||
icon={<DeleteIcon/>}
|
||||
id='delete'
|
||||
name='Delete'
|
||||
onClick={() => mutator.deleteBlock(block)}
|
||||
name={intl.formatMessage({id: 'ContentBlock.Delete', defaultMessage: 'Delete'})}
|
||||
onClick={() => {
|
||||
const description = intl.formatMessage({id: 'ContentBlock.DeleteAction', defaultMessage: 'delete'})
|
||||
const contentOrder = contents.map((o) => o.id).filter((o) => o !== block.id)
|
||||
mutator.performAsUndoGroup(async () => {
|
||||
await mutator.deleteBlock(block, description)
|
||||
await mutator.changeCardContentOrder(card, contentOrder, description)
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
|
@ -134,10 +153,9 @@ class ContentBlock extends React.PureComponent<Props> {
|
|||
{block.type === 'text' &&
|
||||
<MarkdownEditor
|
||||
text={block.title}
|
||||
placeholderText='Edit text...'
|
||||
placeholderText={intl.formatMessage({id: 'ContentBlock.editText', defaultMessage: 'Edit text...'})}
|
||||
onBlur={(text) => {
|
||||
Utils.log(`change text ${block.id}, ${text}`)
|
||||
mutator.changeTitle(block, text, 'edit card text')
|
||||
mutator.changeTitle(block, text, intl.formatMessage({id: 'ContentBlock.editCardText', defaultMessage: 'edit card text'}))
|
||||
}}
|
||||
readonly={this.props.readonly}
|
||||
/>}
|
||||
|
@ -152,4 +170,4 @@ class ContentBlock extends React.PureComponent<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default ContentBlock
|
||||
export default injectIntl(ContentBlock)
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {BlockIcons} from './blockIcons'
|
||||
import {IBlock, MutableBlock} from './blocks/block'
|
||||
import {Board, IPropertyOption, IPropertyTemplate, MutableBoard, PropertyType} from './blocks/board'
|
||||
import {BoardView, ISortOption, MutableBoardView} from './blocks/boardView'
|
||||
import {Card, MutableCard} from './blocks/card'
|
||||
import {MutableImageBlock} from './blocks/imageBlock'
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './blocks/orderedBlock'
|
||||
import {BoardTree} from './viewModel/boardTree'
|
||||
import {FilterGroup} from './filterGroup'
|
||||
import octoClient from './octoClient'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
import undoManager from './undomanager'
|
||||
import {Utils} from './utils'
|
||||
import {OctoUtils} from './octoUtils'
|
||||
import {BlockIcons} from './blockIcons'
|
||||
import {BoardTree} from './viewModel/boardTree'
|
||||
|
||||
//
|
||||
// The Mutator is used to make all changes to server state
|
||||
|
@ -173,10 +172,10 @@ class Mutator {
|
|||
await this.updateBlock(newBoard, board, actionDescription)
|
||||
}
|
||||
|
||||
async changeOrder(block: IOrderedBlock, order: number, description = 'change order') {
|
||||
const newBlock = new MutableOrderedBlock(block)
|
||||
newBlock.order = order
|
||||
await this.updateBlock(newBlock, block, description)
|
||||
async changeCardContentOrder(card: Card, contentOrder: string[], description = 'reorder'): Promise<void> {
|
||||
const newCard = new MutableCard(card)
|
||||
newCard.contentOrder = contentOrder
|
||||
await this.updateBlock(newCard, card, description)
|
||||
}
|
||||
|
||||
// Property Templates
|
||||
|
@ -583,7 +582,7 @@ class Mutator {
|
|||
return octoClient.importFullArchive(blocks)
|
||||
}
|
||||
|
||||
async createImageBlock(parent: IBlock, file: File, order = 1000): Promise<IBlock | undefined> {
|
||||
async createImageBlock(parent: IBlock, file: File, description = 'add image'): Promise<IBlock | undefined> {
|
||||
const url = await octoClient.uploadFile(file)
|
||||
if (!url) {
|
||||
return undefined
|
||||
|
@ -592,7 +591,6 @@ class Mutator {
|
|||
const block = new MutableImageBlock()
|
||||
block.parentId = parent.id
|
||||
block.rootId = parent.rootId
|
||||
block.order = order
|
||||
block.url = url
|
||||
|
||||
await undoManager.perform(
|
||||
|
@ -602,7 +600,7 @@ class Mutator {
|
|||
async () => {
|
||||
await octoClient.deleteBlock(block.id)
|
||||
},
|
||||
'add image',
|
||||
description,
|
||||
this.undoGroupId,
|
||||
)
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import {MutableCard} from './blocks/card'
|
|||
import {MutableCommentBlock} from './blocks/commentBlock'
|
||||
import {MutableDividerBlock} from './blocks/dividerBlock'
|
||||
import {MutableImageBlock} from './blocks/imageBlock'
|
||||
import {IOrderedBlock} from './blocks/orderedBlock'
|
||||
import {MutableTextBlock} from './blocks/textBlock'
|
||||
import {Utils} from './utils'
|
||||
|
||||
|
@ -42,22 +41,27 @@ class OctoUtils {
|
|||
return displayValue
|
||||
}
|
||||
|
||||
static getOrderBefore(block: IOrderedBlock, blocks: readonly IOrderedBlock[]): number {
|
||||
const index = blocks.indexOf(block)
|
||||
if (index === 0) {
|
||||
return block.order / 2
|
||||
static relativeBlockOrder(partialOrder: readonly string[], blocks: readonly IBlock[], blockA: IBlock, blockB: IBlock): number {
|
||||
const orderA = partialOrder.indexOf(blockA.id)
|
||||
const orderB = partialOrder.indexOf(blockB.id)
|
||||
|
||||
if (orderA >= 0 && orderB >= 0) {
|
||||
// Order of both blocks is specified
|
||||
return orderA - orderB
|
||||
}
|
||||
const previousBlock = blocks[index - 1]
|
||||
return (block.order + previousBlock.order) / 2
|
||||
if (orderA >= 0) {
|
||||
return -1
|
||||
}
|
||||
if (orderB >= 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Order of both blocks are unspecified, use create date
|
||||
return blockA.createAt - blockB.createAt
|
||||
}
|
||||
|
||||
static getOrderAfter(block: IOrderedBlock, blocks: readonly IOrderedBlock[]): number {
|
||||
const index = blocks.indexOf(block)
|
||||
if (index === blocks.length - 1) {
|
||||
return block.order + 1000
|
||||
}
|
||||
const nextBlock = blocks[index + 1]
|
||||
return (block.order + nextBlock.order) / 2
|
||||
static getBlockOrder(partialOrder: readonly string[], blocks: readonly IBlock[]): IBlock[] {
|
||||
return blocks.slice().sort((a, b) => this.relativeBlockOrder(partialOrder, blocks, a, b))
|
||||
}
|
||||
|
||||
static hydrateBlock(block: IBlock): MutableBlock {
|
||||
|
|
|
@ -99,7 +99,6 @@ class TestBlockFactory {
|
|||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.title = 'title'
|
||||
block.order = 100
|
||||
|
||||
return block
|
||||
}
|
||||
|
@ -109,7 +108,6 @@ class TestBlockFactory {
|
|||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.url = 'url'
|
||||
block.order = 100
|
||||
|
||||
return block
|
||||
}
|
||||
|
@ -119,7 +117,6 @@ class TestBlockFactory {
|
|||
block.parentId = card.id
|
||||
block.rootId = card.rootId
|
||||
block.title = 'title'
|
||||
block.order = 100
|
||||
|
||||
return block
|
||||
}
|
||||
|
|
|
@ -76,6 +76,10 @@ class Utils {
|
|||
return text
|
||||
}
|
||||
|
||||
static sleep(miliseconds: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, miliseconds))
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
static assertValue(valueObject: any): void {
|
||||
|
@ -200,6 +204,10 @@ class Utils {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static arrayMove(arr: any[], srcIndex: number, destIndex: number): void {
|
||||
arr.splice(destIndex, 0, arr.splice(srcIndex, 1)[0])
|
||||
}
|
||||
}
|
||||
|
||||
export {Utils}
|
||||
|
|
|
@ -8,6 +8,8 @@ import 'isomorphic-fetch'
|
|||
import {TestBlockFactory} from '../test/testBlockFactory'
|
||||
import {FetchMock} from '../test/fetchMock'
|
||||
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import {CardTree, MutableCardTree} from './cardTree'
|
||||
|
||||
global.fetch = FetchMock.fn
|
||||
|
@ -20,9 +22,14 @@ test('CardTree', async () => {
|
|||
const card = TestBlockFactory.createCard()
|
||||
expect(card.id).not.toBeNull()
|
||||
const comment = TestBlockFactory.createComment(card)
|
||||
|
||||
// Content
|
||||
const text = TestBlockFactory.createText(card)
|
||||
await Utils.sleep(10)
|
||||
const image = TestBlockFactory.createImage(card)
|
||||
await Utils.sleep(10)
|
||||
const divider = TestBlockFactory.createDivider(card)
|
||||
card.contentOrder = [image.id, divider.id, text.id]
|
||||
|
||||
let cardTree: CardTree | undefined
|
||||
|
||||
|
@ -41,12 +48,14 @@ test('CardTree', async () => {
|
|||
expect(FetchMock.fn).toBeCalledTimes(2)
|
||||
expect(cardTree.card).toEqual(card)
|
||||
expect(cardTree.comments).toEqual([comment])
|
||||
expect(cardTree.contents).toEqual([text, image, divider])
|
||||
expect(cardTree.contents).toEqual([image, divider, text]) // Must match specified card.contentOrder
|
||||
|
||||
// Incremental update
|
||||
const comment2 = TestBlockFactory.createComment(card)
|
||||
const text2 = TestBlockFactory.createText(card)
|
||||
await Utils.sleep(10)
|
||||
const image2 = TestBlockFactory.createImage(card)
|
||||
await Utils.sleep(10)
|
||||
const divider2 = TestBlockFactory.createDivider(card)
|
||||
|
||||
cardTree = MutableCardTree.incrementalUpdate(cardTree, [comment2, text2, image2, divider2])
|
||||
|
@ -55,7 +64,9 @@ test('CardTree', async () => {
|
|||
fail('incrementalUpdate')
|
||||
}
|
||||
expect(cardTree.comments).toEqual([comment, comment2])
|
||||
expect(cardTree.contents).toEqual([text, image, divider, text2, image2, divider2])
|
||||
|
||||
// The added content's order was not specified in card.contentOrder, so much match created date order
|
||||
expect(cardTree.contents).toEqual([image, divider, text, text2, image2, divider2])
|
||||
|
||||
// Incremental update: No change
|
||||
const anotherCard = TestBlockFactory.createCard()
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {Card, MutableCard} from '../blocks/card'
|
||||
import {IOrderedBlock} from '../blocks/orderedBlock'
|
||||
import {IContentBlock} from '../blocks/contentBlock'
|
||||
import octoClient from '../octoClient'
|
||||
import {OctoUtils} from '../octoUtils'
|
||||
|
||||
interface CardTree {
|
||||
readonly card: Card
|
||||
readonly comments: readonly IBlock[]
|
||||
readonly contents: readonly IOrderedBlock[]
|
||||
readonly contents: readonly IContentBlock[]
|
||||
readonly allBlocks: readonly IBlock[]
|
||||
}
|
||||
|
||||
class MutableCardTree implements CardTree {
|
||||
card: MutableCard
|
||||
comments: IBlock[] = []
|
||||
contents: IOrderedBlock[] = []
|
||||
contents: IContentBlock[] = []
|
||||
|
||||
get allBlocks(): IBlock[] {
|
||||
return [this.card, ...this.comments, ...this.contents]
|
||||
|
@ -55,8 +55,8 @@ class MutableCardTree implements CardTree {
|
|||
filter((block) => block.type === 'comment').
|
||||
sort((a, b) => a.createAt - b.createAt)
|
||||
|
||||
const contentBlocks = blocks.filter((block) => block.type === 'text' || block.type === 'image' || block.type === 'divider') as IOrderedBlock[]
|
||||
cardTree.contents = contentBlocks.sort((a, b) => a.order - b.order)
|
||||
const contentBlocks = blocks.filter((block) => block.type === 'text' || block.type === 'image' || block.type === 'divider') as IContentBlock[]
|
||||
cardTree.contents = OctoUtils.getBlockOrder(card.contentOrder, contentBlocks)
|
||||
|
||||
return cardTree
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue