mirror of
https://github.com/mattermost/focalboard.git
synced 2024-11-24 08:22:29 +02:00
npm run fix and replaced tabs with spaces
This commit is contained in:
parent
262f3c043d
commit
a8a274ff0f
@ -1,6 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import { IBlock } from '../blocks/block'
|
||||
import {IBlock} from '../blocks/block'
|
||||
|
||||
import {MutableBlock} from './block'
|
||||
|
||||
type PropertyType = 'text' | 'number' | 'select' | 'multiSelect' | 'date' | 'person' | 'file' | 'checkbox' | 'url' | 'email' | 'phone' | 'createdTime' | 'createdBy' | 'updatedTime' | 'updatedBy'
|
||||
|
@ -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 {IBlock} from '../blocks/block'
|
||||
import {FilterGroup} from '../filterGroup'
|
||||
|
||||
import {MutableBlock} from './block'
|
||||
|
@ -1,10 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import { IBlock } from '../blocks/block'
|
||||
import {IBlock} from '../blocks/block'
|
||||
|
||||
import {MutableBlock} from './block'
|
||||
|
||||
interface CommentBlock extends IBlock {
|
||||
}
|
||||
type CommentBlock = IBlock
|
||||
|
||||
class MutableCommentBlock extends MutableBlock {
|
||||
constructor(block: any = {}) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import { IOrderedBlock, MutableOrderedBlock } from './orderedBlock'
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './orderedBlock'
|
||||
|
||||
interface ImageBlock extends IOrderedBlock {
|
||||
readonly url: string
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { IBlock } from "../blocks/block"
|
||||
import { MutableBlock } from "./block"
|
||||
// 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
|
||||
|
@ -1,10 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import { IOrderedBlock, MutableOrderedBlock } from './orderedBlock'
|
||||
import {IOrderedBlock, MutableOrderedBlock} from './orderedBlock'
|
||||
|
||||
interface TextBlock extends IOrderedBlock {
|
||||
|
||||
}
|
||||
type TextBlock = IOrderedBlock
|
||||
|
||||
class MutableTextBlock extends MutableOrderedBlock {
|
||||
constructor(block: any = {}) {
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import { MutableBlock } from '../blocks/block'
|
||||
|
||||
import {MutableBlock} from '../blocks/block'
|
||||
|
||||
import {IPropertyTemplate} from '../blocks/board'
|
||||
import {Card} from '../blocks/card'
|
||||
@ -66,8 +67,8 @@ class BoardCard extends React.Component<BoardCardProps, BoardCardState> {
|
||||
<div key='__title'>{card.title || 'Untitled'}</div>
|
||||
</div>
|
||||
{visiblePropertyTemplates.map((template) => {
|
||||
return OctoUtils.propertyValueReadonlyElement(card, template, '')
|
||||
})}
|
||||
return OctoUtils.propertyValueReadonlyElement(card, template, '')
|
||||
})}
|
||||
</div>)
|
||||
|
||||
return element
|
||||
|
@ -1,24 +1,24 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import { BlockIcons } from '../blockIcons'
|
||||
import { MutableCommentBlock } from '../blocks/commentBlock'
|
||||
import { IOrderedBlock } from '../blocks/orderedBlock'
|
||||
import { MutableTextBlock } from '../blocks/textBlock'
|
||||
import { BoardTree } from '../viewModel/boardTree'
|
||||
import { CardTree, MutableCardTree } from '../viewModel/cardTree'
|
||||
import { Menu as OldMenu, MenuOption } from '../menu'
|
||||
|
||||
import {BlockIcons} from '../blockIcons'
|
||||
import {MutableCommentBlock} from '../blocks/commentBlock'
|
||||
import {IOrderedBlock} from '../blocks/orderedBlock'
|
||||
import {MutableTextBlock} from '../blocks/textBlock'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import {CardTree, MutableCardTree} from '../viewModel/cardTree'
|
||||
import {Menu as OldMenu, MenuOption} from '../menu'
|
||||
import mutator from '../mutator'
|
||||
import { OctoListener } from '../octoListener'
|
||||
import { IBlock } from '../blocks/block'
|
||||
import { OctoUtils } from '../octoUtils'
|
||||
import { PropertyMenu } from '../propertyMenu'
|
||||
import { Utils } from '../utils'
|
||||
import {OctoListener} from '../octoListener'
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {OctoUtils} from '../octoUtils'
|
||||
import {PropertyMenu} from '../propertyMenu'
|
||||
import {Utils} from '../utils'
|
||||
|
||||
import Button from './button'
|
||||
import { Editable } from './editable'
|
||||
import { MarkdownEditor } from './markdownEditor'
|
||||
|
||||
|
||||
import {Editable} from './editable'
|
||||
import {MarkdownEditor} from './markdownEditor'
|
||||
|
||||
type Props = {
|
||||
boardTree: BoardTree
|
||||
@ -35,25 +35,25 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
private cardListener?: OctoListener
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
super(props)
|
||||
this.state = {isHoverOnCover: false}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.cardListener = new OctoListener()
|
||||
this.cardListener.open([this.props.cardId], async (blockId) => {
|
||||
this.cardListener.open([this.props.cardId], async (blockId) => {
|
||||
Utils.log(`cardListener.onChanged: ${blockId}`)
|
||||
await cardTree.sync()
|
||||
this.setState({...this.state, cardTree})
|
||||
this.setState({...this.state, cardTree})
|
||||
})
|
||||
const cardTree = new MutableCardTree(this.props.cardId)
|
||||
const cardTree = new MutableCardTree(this.props.cardId)
|
||||
cardTree.sync().then(() => {
|
||||
this.setState({...this.state, cardTree})
|
||||
setTimeout(() => {
|
||||
this.setState({...this.state, cardTree})
|
||||
setTimeout(() => {
|
||||
if (this.titleRef.current) {
|
||||
this.titleRef.current.focus()
|
||||
}
|
||||
}, 0)
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
|
||||
@ -63,17 +63,17 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {boardTree} = this.props
|
||||
const {boardTree} = this.props
|
||||
const {cardTree} = this.state
|
||||
const {board} = boardTree
|
||||
if (!cardTree) {
|
||||
return null
|
||||
if (!cardTree) {
|
||||
return null
|
||||
}
|
||||
const {card, comments} = cardTree
|
||||
|
||||
const newCommentPlaceholderText = 'Add a comment...'
|
||||
|
||||
const backgroundRef = React.createRef<HTMLDivElement>()
|
||||
const backgroundRef = React.createRef<HTMLDivElement>()
|
||||
const newCommentRef = React.createRef<Editable>()
|
||||
const sendCommentButtonRef = React.createRef<HTMLDivElement>()
|
||||
let contentElements
|
||||
@ -81,14 +81,14 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
contentElements =
|
||||
(<div className='octo-content'>
|
||||
{cardTree.contents.map((block) => {
|
||||
if (block.type === 'text') {
|
||||
const cardText = block.title
|
||||
return (<div
|
||||
if (block.type === 'text') {
|
||||
const cardText = block.title
|
||||
return (<div
|
||||
key={block.id}
|
||||
className='octo-block octo-hover-container'
|
||||
>
|
||||
>
|
||||
<div className='octo-block-margin'>
|
||||
<div
|
||||
<div
|
||||
className='octo-button octo-hovercontrol square octo-hover-item'
|
||||
onClick={(e) => {
|
||||
this.showContentBlockMenu(e, block)
|
||||
@ -96,22 +96,24 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
>
|
||||
<div className='imageOptions'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MarkdownEditor
|
||||
text={cardText} placeholderText='Edit text...' onChanged={(text) => {
|
||||
text={cardText}
|
||||
placeholderText='Edit text...'
|
||||
onChanged={(text) => {
|
||||
Utils.log(`change text ${block.id}, ${text}`)
|
||||
mutator.changeTitle(block, text, 'edit card text')
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</div>)
|
||||
} else if (block.type === 'image') {
|
||||
const url = block.fields.url
|
||||
return (<div
|
||||
} else if (block.type === 'image') {
|
||||
const url = block.fields.url
|
||||
return (<div
|
||||
key={block.id}
|
||||
className='octo-block octo-hover-container'
|
||||
>
|
||||
>
|
||||
<div className='octo-block-margin'>
|
||||
<div
|
||||
<div
|
||||
className='octo-button octo-hovercontrol square octo-hover-item'
|
||||
onClick={(e) => {
|
||||
this.showContentBlockMenu(e, block)
|
||||
@ -119,18 +121,18 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
>
|
||||
<div className='imageOptions'/>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
</div>
|
||||
<img
|
||||
src={url}
|
||||
alt={block.title}
|
||||
></img>
|
||||
/>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
return <div/>
|
||||
})}
|
||||
return <div/>
|
||||
})}
|
||||
</div>)
|
||||
} else {
|
||||
} else {
|
||||
contentElements = (<div className='octo-content'>
|
||||
<div className='octo-block octo-hover-container'>
|
||||
<div className='octo-block-margin'/>
|
||||
@ -147,58 +149,58 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
</div>)
|
||||
}
|
||||
}
|
||||
|
||||
const icon = card.icon
|
||||
|
||||
// TODO: Replace this placeholder
|
||||
// TODO: Replace this placeholder
|
||||
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>'
|
||||
|
||||
return (
|
||||
return (
|
||||
<>
|
||||
<div className='content'>
|
||||
<div className='content'>
|
||||
{icon ?
|
||||
<div
|
||||
<div
|
||||
className='octo-button octo-icon octo-card-icon'
|
||||
onClick={(e) => {
|
||||
this.iconClicked(e)
|
||||
}}
|
||||
>{icon}</div> :
|
||||
undefined
|
||||
}
|
||||
}
|
||||
<div
|
||||
className='octo-hovercontrols'
|
||||
onMouseOver={() => {
|
||||
className='octo-hovercontrols'
|
||||
onMouseOver={() => {
|
||||
this.setState({...this.state, isHoverOnCover: true})
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
onMouseLeave={() => {
|
||||
this.setState({...this.state, isHoverOnCover: false})
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
>
|
||||
<Button
|
||||
style={{display: (!icon && this.state.isHoverOnCover) ? null : 'none'}}
|
||||
onClick={() => {
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
mutator.changeIcon(card, newIcon)
|
||||
}}
|
||||
>Add Icon</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Editable
|
||||
ref={this.titleRef}
|
||||
className='title'
|
||||
text={card.title}
|
||||
placeholderText='Untitled'
|
||||
onChanged={(text) => {
|
||||
ref={this.titleRef}
|
||||
className='title'
|
||||
text={card.title}
|
||||
placeholderText='Untitled'
|
||||
onChanged={(text) => {
|
||||
mutator.changeTitle(card, text)
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
|
||||
{/* Property list */}
|
||||
|
||||
<div className='octo-propertylist'>
|
||||
{board.cardProperties.map((propertyTemplate) => {
|
||||
{board.cardProperties.map((propertyTemplate) => {
|
||||
return (
|
||||
<div
|
||||
key={propertyTemplate.id}
|
||||
@ -208,72 +210,72 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
className='octo-button octo-propertyname'
|
||||
onClick={(e) => {
|
||||
const menu = PropertyMenu.shared
|
||||
menu.property = propertyTemplate
|
||||
menu.property = propertyTemplate
|
||||
menu.onNameChanged = (propertyName) => {
|
||||
Utils.log('menu.onNameChanged')
|
||||
mutator.renameProperty(board, propertyTemplate.id, propertyName)
|
||||
Utils.log('menu.onNameChanged')
|
||||
mutator.renameProperty(board, propertyTemplate.id, propertyName)
|
||||
}
|
||||
|
||||
menu.onMenuClicked = async (command) => {
|
||||
switch (command) {
|
||||
case 'type-text':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'text')
|
||||
break
|
||||
case 'type-number':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'number')
|
||||
break
|
||||
case 'type-createdTime':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'createdTime')
|
||||
case 'type-text':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'text')
|
||||
break
|
||||
case 'type-updatedTime':
|
||||
case 'type-number':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'number')
|
||||
break
|
||||
case 'type-createdTime':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'createdTime')
|
||||
break
|
||||
case 'type-updatedTime':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'updatedTime')
|
||||
break
|
||||
case 'type-select':
|
||||
case 'type-select':
|
||||
await mutator.changePropertyType(board, propertyTemplate, 'select')
|
||||
break
|
||||
case 'delete':
|
||||
await mutator.deleteProperty(boardTree, propertyTemplate.id)
|
||||
break
|
||||
default:
|
||||
default:
|
||||
Utils.assertFailure(`Unhandled menu id: ${command}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
menu.showAtElement(e.target as HTMLElement)
|
||||
}}
|
||||
}}
|
||||
>{propertyTemplate.name}</div>
|
||||
{OctoUtils.propertyValueEditableElement(card, propertyTemplate)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
})}
|
||||
|
||||
<div
|
||||
<div
|
||||
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)
|
||||
}}
|
||||
await mutator.insertPropertyTemplate(boardTree)
|
||||
}}
|
||||
>+ Add a property</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Comments */}
|
||||
|
||||
<hr/>
|
||||
<div className='commentlist'>
|
||||
{comments.map((comment) => {
|
||||
{comments.map((comment) => {
|
||||
const optionsButtonRef = React.createRef<HTMLDivElement>()
|
||||
const showCommentMenu = (e: React.MouseEvent, activeComment: IBlock) => {
|
||||
OldMenu.shared.options = [
|
||||
{id: 'delete', name: 'Delete'},
|
||||
OldMenu.shared.options = [
|
||||
{id: 'delete', name: 'Delete'},
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = (id) => {
|
||||
OldMenu.shared.onMenuClicked = (id) => {
|
||||
switch (id) {
|
||||
case 'delete': {
|
||||
mutator.deleteBlock(activeComment)
|
||||
case 'delete': {
|
||||
mutator.deleteBlock(activeComment)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
@ -305,91 +307,91 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
</div>
|
||||
<div className='comment-text'>{comment.title}</div>
|
||||
</div>)
|
||||
})}
|
||||
})}
|
||||
|
||||
{/* New comment */}
|
||||
{/* New comment */}
|
||||
|
||||
<div className='commentrow'>
|
||||
<div className='commentrow'>
|
||||
<img
|
||||
className='comment-avatar'
|
||||
src={userImageUrl}
|
||||
/>
|
||||
className='comment-avatar'
|
||||
src={userImageUrl}
|
||||
/>
|
||||
<Editable
|
||||
ref={newCommentRef}
|
||||
className='newcomment'
|
||||
placeholderText={newCommentPlaceholderText}
|
||||
onChanged={(text) => { }}
|
||||
onFocus={() => {
|
||||
ref={newCommentRef}
|
||||
className='newcomment'
|
||||
placeholderText={newCommentPlaceholderText}
|
||||
onChanged={(text) => { }}
|
||||
onFocus={() => {
|
||||
sendCommentButtonRef.current.style.display = null
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!newCommentRef.current.text) {
|
||||
onBlur={() => {
|
||||
if (!newCommentRef.current.text) {
|
||||
sendCommentButtonRef.current.style.display = 'none'
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
||||
sendCommentButtonRef.current.click()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) {
|
||||
sendCommentButtonRef.current.click()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={sendCommentButtonRef}
|
||||
className='octo-button filled'
|
||||
style={{display: 'none'}}
|
||||
onClick={(e) => {
|
||||
const text = newCommentRef.current.text
|
||||
ref={sendCommentButtonRef}
|
||||
className='octo-button filled'
|
||||
style={{display: 'none'}}
|
||||
onClick={(e) => {
|
||||
const text = newCommentRef.current.text
|
||||
console.log(`Send comment: ${newCommentRef.current.text}`)
|
||||
this.sendComment(text)
|
||||
newCommentRef.current.text = undefined
|
||||
this.sendComment(text)
|
||||
newCommentRef.current.text = undefined
|
||||
newCommentRef.current.blur()
|
||||
}}
|
||||
>Send</div>
|
||||
>Send</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
</div>
|
||||
|
||||
{/* Content blocks */}
|
||||
{/* Content blocks */}
|
||||
|
||||
<div className='content fullwidth'>
|
||||
<div className='content fullwidth'>
|
||||
{contentElements}
|
||||
</div>
|
||||
|
||||
<div className='content'>
|
||||
<div className='content'>
|
||||
<div className='octo-hoverpanel octo-hover-container'>
|
||||
<div
|
||||
<div
|
||||
className='octo-button octo-hovercontrol octo-hover-item'
|
||||
onClick={(e) => {
|
||||
OldMenu.shared.options = [
|
||||
{id: 'text', name: 'Text'},
|
||||
OldMenu.shared.options = [
|
||||
{id: 'text', name: 'Text'},
|
||||
{id: 'image', name: 'Image'},
|
||||
]
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
switch (optionId) {
|
||||
case 'text':
|
||||
const block = new MutableTextBlock()
|
||||
block.parentId = card.id
|
||||
block.order = cardTree.contents.length * 1000
|
||||
await mutator.insertBlock(block, 'add text')
|
||||
break
|
||||
case 'image':
|
||||
break
|
||||
case 'image':
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(card.id, file, cardTree.contents.length * 1000)
|
||||
mutator.createImageBlock(card.id, file, cardTree.contents.length * 1000)
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
break
|
||||
'.jpg,.jpeg,.png')
|
||||
break
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}}
|
||||
>Add content</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -398,56 +400,56 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
|
||||
Utils.assertValue(cardId)
|
||||
|
||||
const block = new MutableCommentBlock({parentId: cardId, title: text})
|
||||
const block = new MutableCommentBlock({parentId: cardId, title: text})
|
||||
await mutator.insertBlock(block, 'add comment')
|
||||
}
|
||||
|
||||
private showContentBlockMenu(e: React.MouseEvent, block: IOrderedBlock) {
|
||||
const {cardTree} = this.state
|
||||
const {cardId} = this.props
|
||||
const index = cardTree.contents.indexOf(block)
|
||||
const {cardId} = this.props
|
||||
const index = cardTree.contents.indexOf(block)
|
||||
|
||||
const options: MenuOption[] = []
|
||||
if (index > 0) {
|
||||
const options: MenuOption[] = []
|
||||
if (index > 0) {
|
||||
options.push({id: 'moveUp', name: 'Move up'})
|
||||
}
|
||||
if (index < cardTree.contents.length - 1) {
|
||||
}
|
||||
if (index < cardTree.contents.length - 1) {
|
||||
options.push({id: 'moveDown', name: 'Move down'})
|
||||
}
|
||||
|
||||
options.push(
|
||||
options.push(
|
||||
{id: 'insertAbove', name: 'Insert above', type: 'submenu'},
|
||||
{id: 'delete', name: 'Delete'},
|
||||
{id: 'delete', name: 'Delete'},
|
||||
)
|
||||
|
||||
OldMenu.shared.options = options
|
||||
OldMenu.shared.subMenuOptions.set('insertAbove', [
|
||||
OldMenu.shared.options = options
|
||||
OldMenu.shared.subMenuOptions.set('insertAbove', [
|
||||
{id: 'text', name: 'Text'},
|
||||
{id: 'image', name: 'Image'},
|
||||
{id: 'image', name: 'Image'},
|
||||
])
|
||||
OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
switch (optionId) {
|
||||
case 'moveUp': {
|
||||
if (index < 1) {
|
||||
Utils.logError(`Unexpected index ${index}`); return
|
||||
}
|
||||
const previousBlock = cardTree.contents[index - 1]
|
||||
const newOrder = OctoUtils.getOrderBefore(previousBlock, cardTree.contents)
|
||||
const previousBlock = cardTree.contents[index - 1]
|
||||
const newOrder = OctoUtils.getOrderBefore(previousBlock, cardTree.contents)
|
||||
Utils.log(`moveUp ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move up')
|
||||
mutator.changeOrder(block, newOrder, 'move up')
|
||||
break
|
||||
}
|
||||
case 'moveDown': {
|
||||
case 'moveDown': {
|
||||
if (index >= cardTree.contents.length - 1) {
|
||||
Utils.logError(`Unexpected index ${index}`); return
|
||||
}
|
||||
const nextBlock = cardTree.contents[index + 1]
|
||||
const newOrder = OctoUtils.getOrderAfter(nextBlock, cardTree.contents)
|
||||
const nextBlock = cardTree.contents[index + 1]
|
||||
const newOrder = OctoUtils.getOrderAfter(nextBlock, cardTree.contents)
|
||||
Utils.log(`moveDown ${newOrder}`)
|
||||
mutator.changeOrder(block, newOrder, 'move down')
|
||||
break
|
||||
}
|
||||
case 'insertAbove-text': {
|
||||
case 'insertAbove-text': {
|
||||
const newBlock = new MutableTextBlock()
|
||||
newBlock.parentId = cardId
|
||||
|
||||
@ -455,24 +457,24 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
newBlock.order = OctoUtils.getOrderBefore(block, cardTree.contents)
|
||||
Utils.log(`insert block ${block.id}, order: ${block.order}`)
|
||||
mutator.insertBlock(newBlock, 'insert card text')
|
||||
break
|
||||
break
|
||||
}
|
||||
case 'insertAbove-image': {
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(cardId, file, OctoUtils.getOrderBefore(block, cardTree.contents))
|
||||
case 'insertAbove-image': {
|
||||
Utils.selectLocalFile(
|
||||
(file) => {
|
||||
mutator.createImageBlock(cardId, file, OctoUtils.getOrderBefore(block, cardTree.contents))
|
||||
},
|
||||
'.jpg,.jpeg,.png')
|
||||
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
mutator.deleteBlock(block)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
private iconClicked(e: React.MouseEvent) {
|
||||
@ -481,18 +483,18 @@ export default class CardDetail extends React.Component<Props, State> {
|
||||
|
||||
OldMenu.shared.options = [
|
||||
{id: 'random', name: 'Random'},
|
||||
{id: 'remove', name: 'Remove Icon'},
|
||||
{id: 'remove', name: 'Remove Icon'},
|
||||
]
|
||||
OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
switch (optionId) {
|
||||
case 'remove':
|
||||
mutator.changeIcon(card, undefined, 'remove icon')
|
||||
break
|
||||
case 'random':
|
||||
break
|
||||
case 'random':
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
mutator.changeIcon(card, newIcon)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ type State = {
|
||||
class Editable extends React.Component<Props, State> {
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
isMarkdown: false,
|
||||
isMarkdown: false,
|
||||
isMultiline: false,
|
||||
}
|
||||
|
||||
@ -36,30 +36,30 @@ class Editable extends React.Component<Props, State> {
|
||||
const {isMarkdown} = this.props
|
||||
|
||||
if (!value) {
|
||||
this.elementRef.current.innerText = ''
|
||||
} else {
|
||||
this.elementRef.current.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value)
|
||||
}
|
||||
this.elementRef.current.innerText = ''
|
||||
} else {
|
||||
this.elementRef.current.innerHTML = isMarkdown ? Utils.htmlFromMarkdown(value) : Utils.htmlEncode(value)
|
||||
}
|
||||
|
||||
this._text = value || ''
|
||||
this._text = value || ''
|
||||
}
|
||||
|
||||
private elementRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this._text = props.text || ''
|
||||
this._text = props.text || ''
|
||||
}
|
||||
|
||||
componentDidUpdate(prevPros: Props, prevState: State) {
|
||||
componentDidUpdate() {
|
||||
this._text = this.props.text || ''
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.elementRef.current.focus()
|
||||
this.elementRef.current.focus()
|
||||
|
||||
// Put cursor at end
|
||||
document.execCommand('selectAll', false, null)
|
||||
document.execCommand('selectAll', false, null)
|
||||
document.getSelection().collapseToEnd()
|
||||
}
|
||||
|
||||
@ -68,15 +68,15 @@ class Editable extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {text, className, style, placeholderText, isMarkdown, isMultiline, onFocus, onBlur, onKeyDown, onChanged} = this.props
|
||||
const {text, className, style, placeholderText, isMarkdown, isMultiline, onFocus, onBlur, onKeyDown, onChanged} = this.props
|
||||
|
||||
const initialStyle = {...this.props.style}
|
||||
const initialStyle = {...this.props.style}
|
||||
|
||||
let html: string
|
||||
if (text) {
|
||||
html = isMarkdown ? Utils.htmlFromMarkdown(text) : Utils.htmlEncode(text)
|
||||
} else {
|
||||
html = ''
|
||||
} else {
|
||||
html = ''
|
||||
}
|
||||
|
||||
const element =
|
||||
@ -91,43 +91,43 @@ class Editable extends React.Component<Props, State> {
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
|
||||
onFocus={() => {
|
||||
this.elementRef.current.innerText = this.text
|
||||
this.elementRef.current.style.color = style?.color || null
|
||||
this.elementRef.current.classList.add('active')
|
||||
this.elementRef.current.innerText = this.text
|
||||
this.elementRef.current.style.color = style?.color || null
|
||||
this.elementRef.current.classList.add('active')
|
||||
|
||||
if (onFocus) {
|
||||
if (onFocus) {
|
||||
onFocus()
|
||||
}
|
||||
}}
|
||||
}}
|
||||
|
||||
onBlur={async () => {
|
||||
const newText = this.elementRef.current.innerText
|
||||
const oldText = this.props.text || ''
|
||||
if (newText !== oldText && onChanged) {
|
||||
onChanged(newText)
|
||||
}
|
||||
const newText = this.elementRef.current.innerText
|
||||
const oldText = this.props.text || ''
|
||||
if (newText !== oldText && onChanged) {
|
||||
onChanged(newText)
|
||||
}
|
||||
|
||||
this.text = newText
|
||||
this.text = newText
|
||||
|
||||
this.elementRef.current.classList.remove('active')
|
||||
if (onBlur) {
|
||||
this.elementRef.current.classList.remove('active')
|
||||
if (onBlur) {
|
||||
onBlur()
|
||||
}
|
||||
}}
|
||||
}}
|
||||
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
|
||||
e.stopPropagation()
|
||||
this.elementRef.current.blur()
|
||||
} else if (!isMultiline && e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
|
||||
e.stopPropagation()
|
||||
this.elementRef.current.blur()
|
||||
}
|
||||
if (e.keyCode === 27 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // ESC
|
||||
e.stopPropagation()
|
||||
this.elementRef.current.blur()
|
||||
} else if (!isMultiline && e.keyCode === 13 && !(e.metaKey || e.ctrlKey) && !e.shiftKey && !e.altKey) { // Return
|
||||
e.stopPropagation()
|
||||
this.elementRef.current.blur()
|
||||
}
|
||||
|
||||
if (onKeyDown) {
|
||||
if (onKeyDown) {
|
||||
onKeyDown(e)
|
||||
}
|
||||
}}
|
||||
}}
|
||||
/>);
|
||||
|
||||
return element
|
||||
|
@ -38,30 +38,30 @@ class MarkdownEditor extends React.Component<Props, State> {
|
||||
private previewRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {isEditing: false}
|
||||
super(props)
|
||||
this.state = {isEditing: false}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevPros: Props, prevState: State) {
|
||||
this.text = this.props.text || ''
|
||||
this.text = this.props.text || ''
|
||||
}
|
||||
|
||||
showEditor() {
|
||||
const cm = this.editorInstance?.codemirror
|
||||
if (cm) {
|
||||
if (cm) {
|
||||
setTimeout(() => {
|
||||
cm.refresh()
|
||||
cm.focus()
|
||||
cm.getInputField()?.focus()
|
||||
cm.setCursor(cm.lineCount(), 0) // Put cursor at end
|
||||
}, 100)
|
||||
cm.setCursor(cm.lineCount(), 0) // Put cursor at end
|
||||
}, 100)
|
||||
}
|
||||
|
||||
this.setState({isEditing: true})
|
||||
this.setState({isEditing: true})
|
||||
}
|
||||
|
||||
hideEditor() {
|
||||
this.editorInstance?.codemirror?.getInputField()?.blur()
|
||||
this.editorInstance?.codemirror?.getInputField()?.blur()
|
||||
this.setState({isEditing: false})
|
||||
}
|
||||
|
||||
@ -71,22 +71,22 @@ class MarkdownEditor extends React.Component<Props, State> {
|
||||
|
||||
let html: string
|
||||
if (text) {
|
||||
html = Utils.htmlFromMarkdown(text)
|
||||
} else {
|
||||
html = Utils.htmlFromMarkdown(text)
|
||||
} else {
|
||||
html = Utils.htmlFromMarkdown(placeholderText || '')
|
||||
}
|
||||
|
||||
const previewElement =
|
||||
const previewElement =
|
||||
(<div
|
||||
ref={this.previewRef}
|
||||
className={text ? 'octo-editor-preview' : 'octo-editor-preview octo-placeholder'}
|
||||
style={{display: isEditing ? 'none' : null}}
|
||||
dangerouslySetInnerHTML={{__html: html}}
|
||||
onClick={() => {
|
||||
if (!isEditing) {
|
||||
this.showEditor()
|
||||
}
|
||||
}}
|
||||
if (!isEditing) {
|
||||
this.showEditor()
|
||||
}
|
||||
}}
|
||||
/>);
|
||||
|
||||
const editorElement =
|
||||
@ -96,75 +96,75 @@ class MarkdownEditor extends React.Component<Props, State> {
|
||||
// Use visibility instead of display here so the editor is pre-rendered, avoiding a flash on showEditor
|
||||
style={isEditing ? {} : {visibility: 'hidden', position: 'absolute', top: 0, left: 0}}
|
||||
onKeyDown={(e) => {
|
||||
// HACKHACK: Need to handle here instad of in CodeMirror because that breaks auto-lists
|
||||
if (e.keyCode === 27 && !e.shiftKey && !(e.ctrlKey || e.metaKey) && !e.altKey) { // Esc
|
||||
this.editorInstance?.codemirror?.getInputField()?.blur()
|
||||
}
|
||||
}}
|
||||
// HACKHACK: Need to handle here instad of in CodeMirror because that breaks auto-lists
|
||||
if (e.keyCode === 27 && !e.shiftKey && !(e.ctrlKey || e.metaKey) && !e.altKey) { // Esc
|
||||
this.editorInstance?.codemirror?.getInputField()?.blur()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SimpleMDE
|
||||
id={uniqueId}
|
||||
ref={this.elementRef}
|
||||
getMdeInstance={(instance) => {
|
||||
this.editorInstance = instance
|
||||
this.editorInstance = instance
|
||||
|
||||
// BUGBUG: This breaks auto-lists
|
||||
// instance.codemirror.setOption("extraKeys", {
|
||||
// "Ctrl-Enter": (cm) => {
|
||||
// cm.getInputField().blur()
|
||||
// }
|
||||
// })
|
||||
}}
|
||||
// BUGBUG: This breaks auto-lists
|
||||
// instance.codemirror.setOption("extraKeys", {
|
||||
// "Ctrl-Enter": (cm) => {
|
||||
// cm.getInputField().blur()
|
||||
// }
|
||||
// })
|
||||
}}
|
||||
value={text}
|
||||
|
||||
// onChange={() => {
|
||||
// // We register a change onBlur, consider implementing "auto-save" later
|
||||
// }}
|
||||
// // We register a change onBlur, consider implementing "auto-save" later
|
||||
// }}
|
||||
events={{
|
||||
blur: () => {
|
||||
const newText = this.elementRef.current.state.value
|
||||
const oldText = this.props.text || ''
|
||||
if (newText !== oldText && onChanged) {
|
||||
const newHtml = newText ? Utils.htmlFromMarkdown(newText) : Utils.htmlFromMarkdown(placeholderText || '')
|
||||
this.previewRef.current.innerHTML = newHtml
|
||||
onChanged(newText)
|
||||
}
|
||||
blur: () => {
|
||||
const newText = this.elementRef.current.state.value
|
||||
const oldText = this.props.text || ''
|
||||
if (newText !== oldText && onChanged) {
|
||||
const newHtml = newText ? Utils.htmlFromMarkdown(newText) : Utils.htmlFromMarkdown(placeholderText || '')
|
||||
this.previewRef.current.innerHTML = newHtml
|
||||
onChanged(newText)
|
||||
}
|
||||
|
||||
this.text = newText
|
||||
this.text = newText
|
||||
|
||||
this.frameRef.current.classList.remove('active')
|
||||
this.frameRef.current.classList.remove('active')
|
||||
|
||||
if (onBlur) {
|
||||
if (onBlur) {
|
||||
onBlur()
|
||||
}
|
||||
|
||||
this.hideEditor()
|
||||
},
|
||||
focus: () => {
|
||||
this.frameRef.current.classList.add('active')
|
||||
this.hideEditor()
|
||||
},
|
||||
focus: () => {
|
||||
this.frameRef.current.classList.add('active')
|
||||
|
||||
this.elementRef.current.setState({value: this.text})
|
||||
this.elementRef.current.setState({value: this.text})
|
||||
|
||||
if (onFocus) {
|
||||
if (onFocus) {
|
||||
onFocus()
|
||||
}
|
||||
},
|
||||
}}
|
||||
},
|
||||
}}
|
||||
options={{
|
||||
autoDownloadFontAwesome: true,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
spellChecker: false,
|
||||
minHeight: '10px',
|
||||
shortcuts: {
|
||||
toggleStrikethrough: 'Cmd-.',
|
||||
togglePreview: null,
|
||||
drawImage: null,
|
||||
drawLink: null,
|
||||
toggleSideBySide: null,
|
||||
toggleFullScreen: null,
|
||||
},
|
||||
}}
|
||||
autoDownloadFontAwesome: true,
|
||||
toolbar: false,
|
||||
status: false,
|
||||
spellChecker: false,
|
||||
minHeight: '10px',
|
||||
shortcuts: {
|
||||
toggleStrikethrough: 'Cmd-.',
|
||||
togglePreview: null,
|
||||
drawImage: null,
|
||||
drawLink: null,
|
||||
toggleSideBySide: null,
|
||||
toggleFullScreen: null,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>)
|
||||
|
||||
|
@ -13,11 +13,11 @@ export default class RootPortal extends React.PureComponent<Props> {
|
||||
el: HTMLDivElement
|
||||
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
children: PropTypes.node,
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
super(props)
|
||||
this.el = document.createElement('div')
|
||||
}
|
||||
|
||||
@ -25,20 +25,20 @@ export default class RootPortal extends React.PureComponent<Props> {
|
||||
const rootPortal = document.getElementById('root-portal')
|
||||
if (rootPortal) {
|
||||
rootPortal.appendChild(this.el)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const rootPortal = document.getElementById('root-portal')
|
||||
if (rootPortal) {
|
||||
const rootPortal = document.getElementById('root-portal')
|
||||
if (rootPortal) {
|
||||
rootPortal.removeChild(this.el)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return ReactDOM.createPortal(
|
||||
this.props.children,
|
||||
this.props.children,
|
||||
this.el,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import { Archiver } from '../archiver'
|
||||
import { Board, MutableBoard } from '../blocks/board'
|
||||
import { BoardTree } from '../viewModel/boardTree'
|
||||
|
||||
import {Archiver} from '../archiver'
|
||||
import {Board, MutableBoard} from '../blocks/board'
|
||||
import {BoardTree} from '../viewModel/boardTree'
|
||||
import mutator from '../mutator'
|
||||
import Menu from '../widgets/menu'
|
||||
import MenuWrapper from '../widgets/menuWrapper'
|
||||
import { WorkspaceTree } from '../viewModel/workspaceTree'
|
||||
import { BoardView } from '../blocks/boardView'
|
||||
|
||||
import {WorkspaceTree} from '../viewModel/workspaceTree'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
|
||||
type Props = {
|
||||
showBoard: (id: string) => void
|
||||
@ -32,11 +32,16 @@ class Sidebar extends React.Component<Props> {
|
||||
{
|
||||
boards.map((board) => {
|
||||
const displayTitle = board.title || '(Untitled Board)'
|
||||
const boardViews = views.filter(view => view.parentId === board.id)
|
||||
const boardViews = views.filter((view) => view.parentId === board.id)
|
||||
return (
|
||||
<div key={board.id}>
|
||||
<div className='octo-sidebar-item octo-hover-container'>
|
||||
<div className='octo-sidebar-title' onClick={() => { this.boardClicked(board) }}>
|
||||
<div
|
||||
className='octo-sidebar-title'
|
||||
onClick={() => {
|
||||
this.boardClicked(board)
|
||||
}}
|
||||
>
|
||||
{board.icon ? `${board.icon} ${displayTitle}` : displayTitle}
|
||||
</div>
|
||||
<div className='octo-spacer'/>
|
||||
@ -63,12 +68,20 @@ class Sidebar extends React.Component<Props> {
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
</div>
|
||||
{boardViews.map(view => {
|
||||
return <div key={view.id} className='octo-sidebar-item subitem octo-hover-container'>
|
||||
<div className='octo-sidebar-title' onClick={() => { this.viewClicked(board, view) }}>
|
||||
{boardViews.map((view) => {
|
||||
return (<div
|
||||
key={view.id}
|
||||
className='octo-sidebar-item subitem octo-hover-container'
|
||||
>
|
||||
<div
|
||||
className='octo-sidebar-title'
|
||||
onClick={() => {
|
||||
this.viewClicked(board, view)
|
||||
}}
|
||||
>
|
||||
{view.title || '(Untitled View)'}
|
||||
</div>
|
||||
</div>
|
||||
</div>)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
@ -136,4 +149,4 @@ class Sidebar extends React.Component<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export { Sidebar }
|
||||
export {Sidebar}
|
||||
|
@ -24,23 +24,23 @@ class Switch extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = {isOn: props.isOn}
|
||||
this.state = {isOn: props.isOn}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.elementRef.current.focus()
|
||||
this.elementRef.current.focus()
|
||||
|
||||
// Put cursor at end
|
||||
document.execCommand('selectAll', false, null)
|
||||
document.getSelection().collapseToEnd()
|
||||
document.getSelection().collapseToEnd()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {style} = this.props
|
||||
const {isOn} = this.state
|
||||
const {isOn} = this.state
|
||||
|
||||
const className = isOn ? 'octo-switch on' : 'octo-switch'
|
||||
const element =
|
||||
const element =
|
||||
(<div
|
||||
ref={this.elementRef}
|
||||
className={className}
|
||||
@ -59,12 +59,12 @@ class Switch extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
private async onClicked() {
|
||||
const newIsOn = !this.state.isOn
|
||||
this.setState({isOn: newIsOn})
|
||||
const newIsOn = !this.state.isOn
|
||||
this.setState({isOn: newIsOn})
|
||||
|
||||
const {onChanged} = this.props
|
||||
const {onChanged} = this.props
|
||||
|
||||
onChanged(newIsOn)
|
||||
onChanged(newIsOn)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,26 +48,26 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevPros: Props, prevState: State) {
|
||||
if (this.state.isSearching && !prevState.isSearching) {
|
||||
if (this.state.isSearching && !prevState.isSearching) {
|
||||
this.searchFieldRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {boardTree, showView} = this.props
|
||||
const {boardTree, showView} = this.props
|
||||
|
||||
if (!boardTree || !boardTree.board) {
|
||||
if (!boardTree || !boardTree.board) {
|
||||
return (
|
||||
<div>Loading...</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const {board, cards, activeView} = boardTree
|
||||
const {board, cards, activeView} = boardTree
|
||||
|
||||
const hasFilter = activeView.filter && activeView.filter.filters?.length > 0
|
||||
const hasSort = activeView.sortOptions.length > 0
|
||||
const hasSort = activeView.sortOptions.length > 0
|
||||
|
||||
this.cardIdToRowMap.clear()
|
||||
this.cardIdToRowMap.clear()
|
||||
|
||||
return (
|
||||
<div className='octo-app'>
|
||||
@ -92,7 +92,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
<Button
|
||||
style={{display: (!board.icon && this.state.isHoverOnCover) ? null : 'none'}}
|
||||
onClick={() => {
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
const newIcon = BlockIcons.shared.randomIcon()
|
||||
mutator.changeIcon(board, newIcon)
|
||||
}}
|
||||
>Add Icon</Button>
|
||||
@ -268,19 +268,19 @@ class TableComponent extends React.Component<Props, State> {
|
||||
const openButonRef = React.createRef<HTMLDivElement>()
|
||||
const tableRowRef = React.createRef<TableRow>()
|
||||
|
||||
let focusOnMount = false
|
||||
let focusOnMount = false
|
||||
if (this.cardIdToFocusOnRender && this.cardIdToFocusOnRender === card.id) {
|
||||
this.cardIdToFocusOnRender = undefined
|
||||
this.cardIdToFocusOnRender = undefined
|
||||
focusOnMount = true
|
||||
}
|
||||
}
|
||||
|
||||
const tableRow = (<TableRow
|
||||
key={card.id}
|
||||
ref={tableRowRef}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
focusOnMount={focusOnMount}
|
||||
onKeyDown={(e) => {
|
||||
const tableRow = (<TableRow
|
||||
key={card.id}
|
||||
ref={tableRowRef}
|
||||
boardTree={boardTree}
|
||||
card={card}
|
||||
focusOnMount={focusOnMount}
|
||||
onKeyDown={(e) => {
|
||||
if (e.keyCode === 13) {
|
||||
// Enter: Insert new card if on last row
|
||||
if (cards.length > 0 && cards[cards.length - 1] === card) {
|
||||
@ -288,7 +288,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>)
|
||||
/>)
|
||||
|
||||
this.cardIdToRowMap.set(card.id, tableRowRef)
|
||||
|
||||
@ -311,7 +311,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private async propertiesClicked(e: React.MouseEvent) {
|
||||
@ -319,25 +319,25 @@ class TableComponent extends React.Component<Props, State> {
|
||||
const {activeView} = boardTree
|
||||
|
||||
const selectProperties = boardTree.board.cardProperties
|
||||
OldMenu.shared.options = selectProperties.map((o) => {
|
||||
OldMenu.shared.options = selectProperties.map((o) => {
|
||||
const isVisible = activeView.visiblePropertyIds.includes(o.id)
|
||||
return {id: o.id, name: o.name, type: 'switch', isOn: isVisible}
|
||||
return {id: o.id, name: o.name, type: 'switch', isOn: isVisible}
|
||||
})
|
||||
|
||||
OldMenu.shared.onMenuToggled = async (id: string, isOn: boolean) => {
|
||||
OldMenu.shared.onMenuToggled = async (id: string, isOn: boolean) => {
|
||||
const property = selectProperties.find((o) => o.id === id)
|
||||
Utils.assertValue(property)
|
||||
Utils.log(`Toggle property ${property.name} ${isOn}`)
|
||||
Utils.log(`Toggle property ${property.name} ${isOn}`)
|
||||
|
||||
let newVisiblePropertyIds = []
|
||||
if (activeView.visiblePropertyIds.includes(id)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== id)
|
||||
} else {
|
||||
if (activeView.visiblePropertyIds.includes(id)) {
|
||||
newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== id)
|
||||
} else {
|
||||
newVisiblePropertyIds = [...activeView.visiblePropertyIds, id]
|
||||
}
|
||||
await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
private filterClicked(e: React.MouseEvent) {
|
||||
@ -347,21 +347,21 @@ class TableComponent extends React.Component<Props, State> {
|
||||
private async optionsClicked(e: React.MouseEvent) {
|
||||
const {boardTree} = this.props
|
||||
|
||||
OldMenu.shared.options = [
|
||||
OldMenu.shared.options = [
|
||||
{id: 'exportCsv', name: 'Export to CSV'},
|
||||
{id: 'exportBoardArchive', name: 'Export board archive'},
|
||||
]
|
||||
]
|
||||
|
||||
OldMenu.shared.onMenuClicked = async (id: string) => {
|
||||
switch (id) {
|
||||
case 'exportCsv': {
|
||||
CsvExporter.exportTableCsv(boardTree)
|
||||
CsvExporter.exportTableCsv(boardTree)
|
||||
break
|
||||
}
|
||||
case 'exportBoardArchive': {
|
||||
Archiver.exportBoardTree(boardTree)
|
||||
case 'exportBoardArchive': {
|
||||
Archiver.exportBoardTree(boardTree)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
@ -369,61 +369,61 @@ class TableComponent extends React.Component<Props, State> {
|
||||
|
||||
private async headerClicked(e: React.MouseEvent<HTMLDivElement>, templateId: string) {
|
||||
const {boardTree} = this.props
|
||||
const {board} = boardTree
|
||||
const {board} = boardTree
|
||||
const {activeView} = boardTree
|
||||
|
||||
const options = [
|
||||
const options = [
|
||||
{id: 'sortAscending', name: 'Sort ascending'},
|
||||
{id: 'sortDescending', name: 'Sort descending'},
|
||||
{id: 'insertLeft', name: 'Insert left'},
|
||||
{id: 'insertRight', name: 'Insert right'},
|
||||
]
|
||||
|
||||
if (templateId !== '__name') {
|
||||
if (templateId !== '__name') {
|
||||
options.push({id: 'hide', name: 'Hide'})
|
||||
options.push({id: 'duplicate', name: 'Duplicate'})
|
||||
options.push({id: 'delete', name: 'Delete'})
|
||||
options.push({id: 'duplicate', name: 'Duplicate'})
|
||||
options.push({id: 'delete', name: 'Delete'})
|
||||
}
|
||||
|
||||
OldMenu.shared.options = options
|
||||
OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => {
|
||||
switch (optionId) {
|
||||
case 'sortAscending': {
|
||||
const newSortOptions = [
|
||||
switch (optionId) {
|
||||
case 'sortAscending': {
|
||||
const newSortOptions = [
|
||||
{propertyId: templateId, reversed: false},
|
||||
]
|
||||
await mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
break
|
||||
}
|
||||
case 'sortDescending': {
|
||||
const newSortOptions = [
|
||||
{propertyId: templateId, reversed: true},
|
||||
]
|
||||
await mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
]
|
||||
await mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
break
|
||||
}
|
||||
case 'insertLeft': {
|
||||
case 'sortDescending': {
|
||||
const newSortOptions = [
|
||||
{propertyId: templateId, reversed: true},
|
||||
]
|
||||
await mutator.changeViewSortOptions(activeView, newSortOptions)
|
||||
break
|
||||
}
|
||||
case 'insertLeft': {
|
||||
if (templateId !== '__name') {
|
||||
const index = board.cardProperties.findIndex((o) => o.id === templateId)
|
||||
await mutator.insertPropertyTemplate(boardTree, index)
|
||||
const index = board.cardProperties.findIndex((o) => o.id === templateId)
|
||||
await mutator.insertPropertyTemplate(boardTree, index)
|
||||
} else {
|
||||
// TODO: Handle name column
|
||||
}
|
||||
// TODO: Handle name column
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'insertRight': {
|
||||
if (templateId !== '__name') {
|
||||
const index = board.cardProperties.findIndex((o) => o.id === templateId) + 1
|
||||
await mutator.insertPropertyTemplate(boardTree, index)
|
||||
} else {
|
||||
// TODO: Handle name column
|
||||
await mutator.insertPropertyTemplate(boardTree, index)
|
||||
} else {
|
||||
// TODO: Handle name column
|
||||
}
|
||||
break
|
||||
break
|
||||
}
|
||||
case 'duplicate': {
|
||||
await mutator.duplicatePropertyTemplate(boardTree, templateId)
|
||||
break
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'hide': {
|
||||
const newVisiblePropertyIds = activeView.visiblePropertyIds.filter((o) => o !== templateId)
|
||||
await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds)
|
||||
@ -431,64 +431,64 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
case 'delete': {
|
||||
await mutator.deleteProperty(boardTree, templateId)
|
||||
break
|
||||
break
|
||||
}
|
||||
default: {
|
||||
Utils.assertFailure(`Unexpected menu option: ${optionId}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
OldMenu.shared.showAtElement(e.target as HTMLElement)
|
||||
}
|
||||
|
||||
focusOnCardTitle(cardId: string) {
|
||||
const tableRowRef = this.cardIdToRowMap.get(cardId)
|
||||
Utils.log(`focusOnCardTitle, ${tableRowRef?.current ?? 'undefined'}`)
|
||||
tableRowRef?.current.focusOnTitle()
|
||||
tableRowRef?.current.focusOnTitle()
|
||||
}
|
||||
|
||||
async addCard(show = false) {
|
||||
const {boardTree} = this.props
|
||||
|
||||
const card = new MutableCard()
|
||||
card.parentId = boardTree.board.id
|
||||
card.icon = BlockIcons.shared.randomIcon()
|
||||
await mutator.insertBlock(
|
||||
card,
|
||||
const card = new MutableCard()
|
||||
card.parentId = boardTree.board.id
|
||||
card.icon = BlockIcons.shared.randomIcon()
|
||||
await mutator.insertBlock(
|
||||
card,
|
||||
'add card',
|
||||
async () => {
|
||||
if (show) {
|
||||
this.setState({shownCard: card})
|
||||
} else {
|
||||
// Focus on this card's title inline on next render
|
||||
} else {
|
||||
// Focus on this card's title inline on next render
|
||||
this.cardIdToFocusOnRender = card.id
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private async onDropToColumn(template: IPropertyTemplate) {
|
||||
const {draggedHeaderTemplate} = this
|
||||
const {draggedHeaderTemplate} = this
|
||||
if (!draggedHeaderTemplate) {
|
||||
return
|
||||
}
|
||||
|
||||
const {boardTree} = this.props
|
||||
const {boardTree} = this.props
|
||||
const {board} = boardTree
|
||||
|
||||
Utils.assertValue(mutator)
|
||||
Utils.assertValue(mutator)
|
||||
Utils.assertValue(boardTree)
|
||||
|
||||
Utils.log(`ondrop. Source column: ${draggedHeaderTemplate.name}, dest column: ${template.name}`)
|
||||
|
||||
// Move template to new index
|
||||
const destIndex = template ? board.cardProperties.indexOf(template) : 0
|
||||
// Move template to new index
|
||||
const destIndex = template ? board.cardProperties.indexOf(template) : 0
|
||||
await mutator.changePropertyTemplateOrder(board, draggedHeaderTemplate, destIndex)
|
||||
}
|
||||
|
||||
onSearchKeyDown(e: React.KeyboardEvent) {
|
||||
if (e.keyCode === 27) { // ESC: Clear search
|
||||
if (e.keyCode === 27) { // ESC: Clear search
|
||||
this.searchFieldRef.current.text = ''
|
||||
this.setState({...this.state, isSearching: false})
|
||||
this.props.setSearchText(undefined)
|
||||
@ -497,7 +497,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
searchChanged(text?: string) {
|
||||
this.props.setSearchText(text)
|
||||
this.props.setSearchText(text)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,12 +30,12 @@ class TableRow extends React.Component<Props, State> {
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.focusOnMount) {
|
||||
this.titleRef.current.focus()
|
||||
}
|
||||
this.titleRef.current.focus()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {boardTree, card, onKeyDown} = this.props
|
||||
const {boardTree, card, onKeyDown} = this.props
|
||||
const {board, activeView} = boardTree
|
||||
|
||||
const openButonRef = React.createRef<HTMLDivElement>()
|
||||
@ -104,7 +104,7 @@ class TableRow extends React.Component<Props, State> {
|
||||
})}
|
||||
</div>)
|
||||
|
||||
return element
|
||||
return element
|
||||
}
|
||||
|
||||
focusOnTitle() {
|
||||
|
@ -17,38 +17,38 @@ type Props = {
|
||||
|
||||
export default class ViewMenu extends React.Component<Props> {
|
||||
handleDeleteView = async (id: string) => {
|
||||
const {board, boardTree, showView} = this.props
|
||||
const {board, boardTree, showView} = this.props
|
||||
Utils.log('deleteView')
|
||||
const view = boardTree.activeView
|
||||
const nextView = boardTree.views.find((o) => o !== view)
|
||||
const view = boardTree.activeView
|
||||
const nextView = boardTree.views.find((o) => o !== view)
|
||||
await mutator.deleteBlock(view, 'delete view')
|
||||
showView(nextView.id)
|
||||
showView(nextView.id)
|
||||
}
|
||||
|
||||
handleViewClick = (id: string) => {
|
||||
const {boardTree, showView} = this.props
|
||||
Utils.log('view ' + id)
|
||||
const view = boardTree.views.find((o) => o.id === id)
|
||||
showView(view.id)
|
||||
const view = boardTree.views.find((o) => o.id === id)
|
||||
showView(view.id)
|
||||
}
|
||||
|
||||
handleAddViewBoard = async (id: string) => {
|
||||
const {board, boardTree, showView} = this.props
|
||||
Utils.log('addview-board')
|
||||
const view = new MutableBoardView()
|
||||
view.title = 'Board View'
|
||||
const view = new MutableBoardView()
|
||||
view.title = 'Board View'
|
||||
view.viewType = 'board'
|
||||
view.parentId = board.id
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
|
||||
await mutator.insertBlock(
|
||||
view,
|
||||
'add view',
|
||||
async () => {
|
||||
view,
|
||||
'add view',
|
||||
async () => {
|
||||
showView(view.id)
|
||||
},
|
||||
async () => {
|
||||
async () => {
|
||||
showView(oldViewId)
|
||||
})
|
||||
}
|
||||
@ -56,19 +56,19 @@ export default class ViewMenu extends React.Component<Props> {
|
||||
handleAddViewTable = async (id: string) => {
|
||||
const {board, boardTree, showView} = this.props
|
||||
|
||||
Utils.log('addview-table')
|
||||
const view = new MutableBoardView()
|
||||
Utils.log('addview-table')
|
||||
const view = new MutableBoardView()
|
||||
view.title = 'Table View'
|
||||
view.viewType = 'table'
|
||||
view.parentId = board.id
|
||||
view.viewType = 'table'
|
||||
view.parentId = board.id
|
||||
view.visiblePropertyIds = board.cardProperties.map((o) => o.id)
|
||||
|
||||
const oldViewId = boardTree.activeView.id
|
||||
const oldViewId = boardTree.activeView.id
|
||||
|
||||
await mutator.insertBlock(
|
||||
view,
|
||||
'add view',
|
||||
async () => {
|
||||
'add view',
|
||||
async () => {
|
||||
showView(view.id)
|
||||
},
|
||||
async () => {
|
||||
@ -77,7 +77,7 @@ export default class ViewMenu extends React.Component<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {boardTree} = this.props
|
||||
const {boardTree} = this.props
|
||||
return (
|
||||
<Menu>
|
||||
{boardTree.views.map((view) => (<Menu.Text
|
||||
|
@ -11,13 +11,13 @@ class FilterClause {
|
||||
|
||||
static filterConditionDisplayString(filterCondition: FilterCondition) {
|
||||
switch (filterCondition) {
|
||||
case 'includes': return 'includes'
|
||||
case 'includes': return 'includes'
|
||||
case 'notIncludes': return "doesn't include"
|
||||
case 'isEmpty': return 'is empty'
|
||||
case 'isEmpty': return 'is empty'
|
||||
case 'isNotEmpty': return 'is not empty'
|
||||
default: {
|
||||
Utils.assertFailure()
|
||||
return '(unknown)'
|
||||
Utils.assertFailure()
|
||||
return '(unknown)'
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -29,10 +29,10 @@ class FilterClause {
|
||||
}
|
||||
|
||||
isEqual(o: FilterClause) {
|
||||
return (
|
||||
return (
|
||||
this.propertyId === o.propertyId &&
|
||||
this.condition === o.condition &&
|
||||
Utils.arraysEqual(this.values, o.values)
|
||||
this.condition === o.condition &&
|
||||
Utils.arraysEqual(this.values, o.values)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -10,12 +10,12 @@ class FilterGroup {
|
||||
filters: (FilterClause | FilterGroup)[] = []
|
||||
|
||||
static isAnInstanceOf(object: any): object is FilterGroup {
|
||||
return 'innerOperation' in object && 'filters' in object
|
||||
return 'innerOperation' in object && 'filters' in object
|
||||
}
|
||||
|
||||
constructor(o: any = {}) {
|
||||
this.operation = o.operation || 'and'
|
||||
this.filters = o.filters ?
|
||||
this.filters = o.filters ?
|
||||
o.filters.map((p: any) => {
|
||||
if (FilterGroup.isAnInstanceOf(p)) {
|
||||
return new FilterGroup(p)
|
||||
|
@ -37,13 +37,13 @@ class Menu {
|
||||
const menuElement = menu.appendChild(Utils.htmlToElement('<div class="menu-options"></div>'))
|
||||
this.appendMenuOptions(menuElement)
|
||||
|
||||
return menu
|
||||
return menu
|
||||
}
|
||||
|
||||
appendMenuOptions(menuElement: HTMLElement) {
|
||||
for (const option of this.options) {
|
||||
if (option.type === 'separator') {
|
||||
const optionElement = menuElement.appendChild(Utils.htmlToElement('<div class="menu-separator"></div>'))
|
||||
for (const option of this.options) {
|
||||
if (option.type === 'separator') {
|
||||
const optionElement = menuElement.appendChild(Utils.htmlToElement('<div class="menu-separator"></div>'))
|
||||
} else {
|
||||
const optionElement = menuElement.appendChild(Utils.htmlToElement('<div class="menu-option"></div>'))
|
||||
optionElement.id = option.id
|
||||
@ -53,62 +53,62 @@ class Menu {
|
||||
optionElement.appendChild(Utils.htmlToElement('<div class="imageSubmenuTriangle" style="float: right;"></div>'))
|
||||
optionElement.onmouseenter = (e) => {
|
||||
// Calculate offset taking window scroll into account
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const rect = optionElement.getBoundingClientRect()
|
||||
this.showSubMenu(rect.right - bodyRect.left, rect.top - bodyRect.top, option.id)
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
if (option.icon) {
|
||||
let iconName: string
|
||||
switch (option.icon) {
|
||||
case 'checked': { iconName = 'imageMenuCheck'; break }
|
||||
switch (option.icon) {
|
||||
case 'checked': { iconName = 'imageMenuCheck'; break }
|
||||
case 'sortUp': { iconName = 'imageMenuSortUp'; break }
|
||||
case 'sortDown': { iconName = 'imageMenuSortDown'; break }
|
||||
default: { Utils.assertFailure(`Unsupported menu icon: ${option.icon}`) }
|
||||
}
|
||||
if (iconName) {
|
||||
optionElement.appendChild(Utils.htmlToElement(`<div class="${iconName}" style="float: right;"></div>`))
|
||||
}
|
||||
}
|
||||
if (iconName) {
|
||||
optionElement.appendChild(Utils.htmlToElement(`<div class="${iconName}" style="float: right;"></div>`))
|
||||
}
|
||||
}
|
||||
|
||||
optionElement.onmouseenter = () => {
|
||||
this.hideSubMenu()
|
||||
optionElement.onmouseenter = () => {
|
||||
this.hideSubMenu()
|
||||
}
|
||||
optionElement.onclick = (e) => {
|
||||
if (this.onMenuClicked) {
|
||||
this.onMenuClicked(option.id, option.type)
|
||||
if (this.onMenuClicked) {
|
||||
this.onMenuClicked(option.id, option.type)
|
||||
}
|
||||
this.hide()
|
||||
e.stopPropagation()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (option.type === 'color') {
|
||||
const colorbox = optionElement.insertBefore(Utils.htmlToElement('<div class="menu-colorbox"></div>'), optionElement.firstChild)
|
||||
colorbox.classList.add(option.id) // id is the css class name for the color
|
||||
colorbox.classList.add(option.id) // id is the css class name for the color
|
||||
} else if (option.type === 'switch') {
|
||||
const className = option.isOn ? 'octo-switch on' : 'octo-switch'
|
||||
const switchElement = optionElement.appendChild(Utils.htmlToElement(`<div class="${className}"></div>`))
|
||||
switchElement.appendChild(Utils.htmlToElement('<div class="octo-switch-inner"></div>'))
|
||||
switchElement.onclick = (e) => {
|
||||
const className = option.isOn ? 'octo-switch on' : 'octo-switch'
|
||||
const switchElement = optionElement.appendChild(Utils.htmlToElement(`<div class="${className}"></div>`))
|
||||
switchElement.appendChild(Utils.htmlToElement('<div class="octo-switch-inner"></div>'))
|
||||
switchElement.onclick = (e) => {
|
||||
const isOn = switchElement.classList.contains('on')
|
||||
if (isOn) {
|
||||
switchElement.classList.remove('on')
|
||||
} else {
|
||||
switchElement.classList.add('on')
|
||||
}
|
||||
} else {
|
||||
switchElement.classList.add('on')
|
||||
}
|
||||
|
||||
if (this.onMenuToggled) {
|
||||
this.onMenuToggled(option.id, !isOn)
|
||||
}
|
||||
e.stopPropagation()
|
||||
}
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
||||
}
|
||||
optionElement.onclick = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showAtElement(element: HTMLElement) {
|
||||
@ -120,46 +120,46 @@ class Menu {
|
||||
}
|
||||
|
||||
showAt(pageX: number, pageY: number) {
|
||||
if (this.menu) {
|
||||
if (this.menu) {
|
||||
this.hide()
|
||||
}
|
||||
this.menu = this.createMenuElement()
|
||||
this.menu.style.left = `${pageX}px`
|
||||
this.menu.style.top = `${pageY}px`
|
||||
this.menu.style.top = `${pageY}px`
|
||||
|
||||
document.body.appendChild(this.menu)
|
||||
|
||||
this.onBodyClick = (e: MouseEvent) => {
|
||||
console.log('onBodyClick')
|
||||
this.hide()
|
||||
}
|
||||
this.onBodyClick = (e: MouseEvent) => {
|
||||
console.log('onBodyClick')
|
||||
this.hide()
|
||||
}
|
||||
|
||||
this.onBodyKeyDown = (e: KeyboardEvent) => {
|
||||
console.log(`onBodyKeyDown, target: ${e.target}`)
|
||||
|
||||
// Ignore keydown events on other elements
|
||||
if (e.target !== document.body) {
|
||||
if (e.target !== document.body) {
|
||||
return
|
||||
}
|
||||
if (e.keyCode === 27) {
|
||||
// ESC
|
||||
if (e.keyCode === 27) {
|
||||
// ESC
|
||||
this.hide()
|
||||
e.stopPropagation()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
document.body.addEventListener('click', this.onBodyClick)
|
||||
document.body.addEventListener('keydown', this.onBodyKeyDown)
|
||||
document.body.addEventListener('keydown', this.onBodyKeyDown)
|
||||
}, 20)
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!this.menu) {
|
||||
if (!this.menu) {
|
||||
return
|
||||
}
|
||||
|
||||
this.hideSubMenu()
|
||||
this.hideSubMenu()
|
||||
|
||||
document.body.removeChild(this.menu)
|
||||
this.menu = undefined
|
||||
@ -167,34 +167,34 @@ class Menu {
|
||||
document.body.removeEventListener('click', this.onBodyClick)
|
||||
this.onBodyClick = undefined
|
||||
|
||||
document.body.removeEventListener('keydown', this.onBodyKeyDown)
|
||||
document.body.removeEventListener('keydown', this.onBodyKeyDown)
|
||||
this.onBodyKeyDown = undefined
|
||||
}
|
||||
|
||||
hideSubMenu() {
|
||||
if (this.subMenu) {
|
||||
this.subMenu.hide()
|
||||
if (this.subMenu) {
|
||||
this.subMenu.hide()
|
||||
this.subMenu = undefined
|
||||
}
|
||||
}
|
||||
|
||||
private showSubMenu(pageX: number, pageY: number, id: string) {
|
||||
console.log(`showSubMenu: ${id}`)
|
||||
console.log(`showSubMenu: ${id}`)
|
||||
const options: MenuOption[] = this.subMenuOptions.get(id) || []
|
||||
|
||||
if (this.subMenu) {
|
||||
if (this.subMenu.options === options) {
|
||||
if (this.subMenu.options === options) {
|
||||
// Already showing the sub menu
|
||||
return
|
||||
return
|
||||
}
|
||||
|
||||
this.subMenu.hide()
|
||||
}
|
||||
this.subMenu.hide()
|
||||
}
|
||||
|
||||
this.subMenu = new Menu()
|
||||
this.subMenu = new Menu()
|
||||
|
||||
this.subMenu.onMenuClicked = (optionId: string, type?: string) => {
|
||||
const subMenuId = `${id}-${optionId}`
|
||||
const subMenuId = `${id}-${optionId}`
|
||||
if (this.onMenuClicked) {
|
||||
this.onMenuClicked(subMenuId, type)
|
||||
}
|
||||
|
@ -97,20 +97,20 @@ class Mutator {
|
||||
}
|
||||
|
||||
async changeIcon(block: Card | Board, icon: string, description = 'change icon') {
|
||||
var newBlock: IBlock
|
||||
let newBlock: IBlock
|
||||
switch (block.type) {
|
||||
case 'card': {
|
||||
const card = new MutableCard(block)
|
||||
card.icon = icon
|
||||
newBlock = card
|
||||
break
|
||||
}
|
||||
case 'board': {
|
||||
const board = new MutableBoard(block)
|
||||
board.icon = icon
|
||||
newBlock = board
|
||||
break
|
||||
}
|
||||
case 'card': {
|
||||
const card = new MutableCard(block)
|
||||
card.icon = icon
|
||||
newBlock = card
|
||||
break
|
||||
}
|
||||
case 'board': {
|
||||
const board = new MutableBoard(block)
|
||||
board.icon = icon
|
||||
newBlock = board
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await this.updateBlock(newBlock, block, description)
|
||||
@ -265,7 +265,7 @@ class Mutator {
|
||||
Utils.assert(board.cardProperties.includes(template))
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === template.id)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)
|
||||
newTemplate.options.push(option)
|
||||
|
||||
await this.updateBlock(newBoard, board, description)
|
||||
@ -275,8 +275,8 @@ class Mutator {
|
||||
const {board} = boardTree
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === template.id)
|
||||
newTemplate.options = newTemplate.options.filter(o => o.value !== option.value)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)
|
||||
newTemplate.options = newTemplate.options.filter((o) => o.value !== option.value)
|
||||
|
||||
await this.updateBlock(newBoard, board, 'delete option')
|
||||
}
|
||||
@ -286,7 +286,7 @@ class Mutator {
|
||||
Utils.log(`srcIndex: ${srcIndex}, destIndex: ${destIndex}`)
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === template.id)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)
|
||||
newTemplate.options.splice(destIndex, 0, newTemplate.options.splice(srcIndex, 1)[0])
|
||||
|
||||
await this.updateBlock(newBoard, board, 'reorder options')
|
||||
@ -299,8 +299,8 @@ class Mutator {
|
||||
const oldBlocks: IBlock[] = [board]
|
||||
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === propertyTemplate.id)
|
||||
const newOption = newTemplate.options.find(o => o.value === oldValue)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === propertyTemplate.id)
|
||||
const newOption = newTemplate.options.find((o) => o.value === oldValue)
|
||||
newOption.value = value
|
||||
const changedBlocks: IBlock[] = [newBoard]
|
||||
|
||||
@ -323,8 +323,8 @@ class Mutator {
|
||||
|
||||
async changePropertyOptionColor(board: Board, template: IPropertyTemplate, option: IPropertyOption, color: string) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === template.id)
|
||||
const newOption = newTemplate.options.find(o => o.value === option.value)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === template.id)
|
||||
const newOption = newTemplate.options.find((o) => o.value === option.value)
|
||||
newOption.color = color
|
||||
await this.updateBlock(newBoard, board, 'change option color')
|
||||
}
|
||||
@ -337,7 +337,7 @@ class Mutator {
|
||||
|
||||
async changePropertyType(board: Board, propertyTemplate: IPropertyTemplate, type: PropertyType) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
const newTemplate = newBoard.cardProperties.find(o => o.id === propertyTemplate.id)
|
||||
const newTemplate = newBoard.cardProperties.find((o) => o.id === propertyTemplate.id)
|
||||
newTemplate.type = type
|
||||
await this.updateBlock(newBoard, board, 'change property type')
|
||||
}
|
||||
@ -356,7 +356,7 @@ class Mutator {
|
||||
await this.updateBlock(newView, view, 'filter')
|
||||
}
|
||||
|
||||
async changeViewVisibleProperties(view: BoardView, visiblePropertyIds: string[], description: string = 'show / hide property') {
|
||||
async changeViewVisibleProperties(view: BoardView, visiblePropertyIds: string[], description = 'show / hide property') {
|
||||
const newView = new MutableBoardView(view)
|
||||
newView.visiblePropertyIds = visiblePropertyIds
|
||||
await this.updateBlock(newView, view, description)
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import { IMutableBlock } from './blocks/block'
|
||||
import {IMutableBlock} from './blocks/block'
|
||||
import {IBlock} from './blocks/block'
|
||||
import {Utils} from './utils'
|
||||
|
||||
@ -11,48 +11,48 @@ class OctoClient {
|
||||
serverUrl: string
|
||||
|
||||
constructor(serverUrl?: string) {
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
console.log(`OctoClient serverUrl: ${this.serverUrl}`)
|
||||
this.serverUrl = serverUrl || window.location.origin
|
||||
console.log(`OctoClient serverUrl: ${this.serverUrl}`)
|
||||
}
|
||||
|
||||
async getSubtree(rootId?: string): Promise<IBlock[]> {
|
||||
const path = `/api/v1/blocks/${rootId}/subtree`
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
const path = `/api/v1/blocks/${rootId}/subtree`
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
}
|
||||
|
||||
async exportFullArchive(): Promise<IBlock[]> {
|
||||
const path = '/api/v1/blocks/export'
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
}
|
||||
|
||||
async importFullArchive(blocks: IBlock[]): Promise<Response> {
|
||||
Utils.log(`importFullArchive: ${blocks.length} blocks(s)`)
|
||||
blocks.forEach((block) => {
|
||||
Utils.log(`\t ${block.type}, ${block.id}`)
|
||||
})
|
||||
})
|
||||
const body = JSON.stringify(blocks)
|
||||
return await fetch(this.serverUrl + '/api/v1/blocks/import', {
|
||||
method: 'POST',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async getBlocksWithParent(parentId: string, type?: string): Promise<IBlock[]> {
|
||||
let path: string
|
||||
if (type) {
|
||||
path = `/api/v1/blocks?parent_id=${encodeURIComponent(parentId)}&type=${encodeURIComponent(type)}`
|
||||
path = `/api/v1/blocks?parent_id=${encodeURIComponent(parentId)}&type=${encodeURIComponent(type)}`
|
||||
} else {
|
||||
path = `/api/v1/blocks?parent_id=${encodeURIComponent(parentId)}`
|
||||
path = `/api/v1/blocks?parent_id=${encodeURIComponent(parentId)}`
|
||||
}
|
||||
return this.getBlocksWithPath(path)
|
||||
}
|
||||
@ -63,20 +63,20 @@ class OctoClient {
|
||||
}
|
||||
|
||||
private async getBlocksWithPath(path: string): Promise<IBlock[]> {
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const response = await fetch(this.serverUrl + path)
|
||||
const blocks = (await response.json() || []) as IMutableBlock[]
|
||||
this.fixBlocks(blocks)
|
||||
return blocks
|
||||
return blocks
|
||||
}
|
||||
|
||||
fixBlocks(blocks: IMutableBlock[]): void {
|
||||
if (!blocks) {
|
||||
if (!blocks) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO
|
||||
// TODO
|
||||
for (const block of blocks) {
|
||||
if (!block.fields) {
|
||||
if (!block.fields) {
|
||||
block.fields = {}
|
||||
}
|
||||
const o = block as any
|
||||
@ -105,12 +105,12 @@ class OctoClient {
|
||||
blocks.forEach((block) => {
|
||||
block.updateAt = now
|
||||
})
|
||||
return await this.insertBlocks(blocks)
|
||||
return await this.insertBlocks(blocks)
|
||||
}
|
||||
|
||||
async deleteBlock(blockId: string): Promise<Response> {
|
||||
console.log(`deleteBlock: ${blockId}`)
|
||||
return await fetch(this.serverUrl + `/api/v1/blocks/${encodeURIComponent(blockId)}`, {
|
||||
console.log(`deleteBlock: ${blockId}`)
|
||||
return await fetch(this.serverUrl + `/api/v1/blocks/${encodeURIComponent(blockId)}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
@ -120,19 +120,19 @@ class OctoClient {
|
||||
}
|
||||
|
||||
async insertBlock(block: IBlock): Promise<Response> {
|
||||
return this.insertBlocks([block])
|
||||
return this.insertBlocks([block])
|
||||
}
|
||||
|
||||
async insertBlocks(blocks: IBlock[]): Promise<Response> {
|
||||
Utils.log(`insertBlocks: ${blocks.length} blocks(s)`)
|
||||
blocks.forEach((block) => {
|
||||
Utils.log(`\t ${block.type}, ${block.id}`)
|
||||
})
|
||||
const body = JSON.stringify(blocks)
|
||||
return await fetch(this.serverUrl + '/api/v1/blocks', {
|
||||
Utils.log(`insertBlocks: ${blocks.length} blocks(s)`)
|
||||
blocks.forEach((block) => {
|
||||
Utils.log(`\t ${block.type}, ${block.id}`)
|
||||
})
|
||||
const body = JSON.stringify(blocks)
|
||||
return await fetch(this.serverUrl + '/api/v1/blocks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
@ -142,33 +142,33 @@ class OctoClient {
|
||||
// Returns URL of uploaded file, or undefined on failure
|
||||
async uploadFile(file: File): Promise<string | undefined> {
|
||||
// IMPORTANT: We need to post the image as a form. The browser will convert this to a application/x-www-form-urlencoded POST
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
try {
|
||||
try {
|
||||
const response = await fetch(this.serverUrl + '/api/v1/files', {
|
||||
method: 'POST',
|
||||
|
||||
// TIPTIP: Leave out Content-Type here, it will be automatically set by the browser
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
})
|
||||
})
|
||||
if (response.status === 200) {
|
||||
try {
|
||||
const text = await response.text()
|
||||
const text = await response.text()
|
||||
Utils.log(`uploadFile response: ${text}`)
|
||||
const json = JSON.parse(text)
|
||||
const json = JSON.parse(text)
|
||||
|
||||
// const json = await response.json()
|
||||
return json.url
|
||||
} catch (e) {
|
||||
Utils.logError(`uploadFile json ERROR: ${e}`)
|
||||
return json.url
|
||||
} catch (e) {
|
||||
Utils.logError(`uploadFile json ERROR: ${e}`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Utils.logError(`uploadFile ERROR: ${e}`)
|
||||
Utils.logError(`uploadFile ERROR: ${e}`)
|
||||
}
|
||||
|
||||
return undefined
|
||||
|
@ -36,64 +36,64 @@ class OctoListener {
|
||||
}
|
||||
|
||||
open(blockIds: string[], onChange: (blockId: string) => void) {
|
||||
let timeoutId: NodeJS.Timeout
|
||||
let timeoutId: NodeJS.Timeout
|
||||
|
||||
if (this.ws) {
|
||||
this.close()
|
||||
this.close()
|
||||
}
|
||||
|
||||
const url = new URL(this.serverUrl)
|
||||
const wsServerUrl = `ws://${url.host}${url.pathname}ws/onchange`
|
||||
const wsServerUrl = `ws://${url.host}${url.pathname}ws/onchange`
|
||||
Utils.log(`OctoListener open: ${wsServerUrl}`)
|
||||
const ws = new WebSocket(wsServerUrl)
|
||||
const ws = new WebSocket(wsServerUrl)
|
||||
this.ws = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
Utils.log(`OctoListener webSocket opened.`)
|
||||
ws.onopen = () => {
|
||||
Utils.log('OctoListener webSocket opened.')
|
||||
this.addBlocks(blockIds)
|
||||
this.isInitialized = true
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (e) => {
|
||||
ws.onerror = (e) => {
|
||||
Utils.logError(`OctoListener websocket onerror. data: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = (e) => {
|
||||
ws.onclose = (e) => {
|
||||
Utils.log(`OctoListener websocket onclose, code: ${e.code}, reason: ${e.reason}`)
|
||||
if (ws === this.ws) {
|
||||
if (ws === this.ws) {
|
||||
// Unexpected close, re-open
|
||||
const reopenBlockIds = this.isInitialized ? this.blockIds.slice() : blockIds.slice()
|
||||
Utils.logError(`Unexpected close, re-opening with ${reopenBlockIds.length} blocks...`)
|
||||
setTimeout(() => {
|
||||
this.open(reopenBlockIds, onChange)
|
||||
}, this.reopenDelay)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
ws.onmessage = (e) => {
|
||||
Utils.log(`OctoListener websocket onmessage. data: ${e.data}`)
|
||||
if (ws !== this.ws) {
|
||||
Utils.log(`Ignoring closed ws`)
|
||||
if (ws !== this.ws) {
|
||||
Utils.log('Ignoring closed ws')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const message = JSON.parse(e.data) as WSMessage
|
||||
switch (message.action) {
|
||||
const message = JSON.parse(e.data) as WSMessage
|
||||
switch (message.action) {
|
||||
case 'UPDATE_BLOCK':
|
||||
if (timeoutId) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = undefined
|
||||
onChange(message.blockId)
|
||||
timeoutId = setTimeout(() => {
|
||||
timeoutId = undefined
|
||||
onChange(message.blockId)
|
||||
}, this.notificationDelay)
|
||||
break
|
||||
default:
|
||||
Utils.logError(`Unexpected action: ${message.action}`)
|
||||
}
|
||||
} catch (e) {
|
||||
Utils.log('message is not an object')
|
||||
Utils.log('message is not an object')
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,13 +115,13 @@ class OctoListener {
|
||||
|
||||
addBlocks(blockIds: string[]): void {
|
||||
if (!this.isOpen) {
|
||||
Utils.assertFailure(`OctoListener.addBlocks: ws is not open`)
|
||||
Utils.assertFailure('OctoListener.addBlocks: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'ADD',
|
||||
blockIds
|
||||
blockIds,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
@ -130,20 +130,20 @@ class OctoListener {
|
||||
|
||||
removeBlocks(blockIds: string[]): void {
|
||||
if (!this.isOpen) {
|
||||
Utils.assertFailure(`OctoListener.removeBlocks: ws is not open`)
|
||||
Utils.assertFailure('OctoListener.removeBlocks: ws is not open')
|
||||
return
|
||||
}
|
||||
|
||||
const command: WSCommand = {
|
||||
action: 'REMOVE',
|
||||
blockIds
|
||||
blockIds,
|
||||
}
|
||||
|
||||
this.ws.send(JSON.stringify(command))
|
||||
|
||||
// Remove registered blockIds, maintinging multiple copies (simple ref-counting)
|
||||
for (let i=0; i<this.blockIds.length; i++) {
|
||||
for (let j=0; j<blockIds.length; j++) {
|
||||
for (let i = 0; i < this.blockIds.length; i++) {
|
||||
for (let j = 0; j < blockIds.length; j++) {
|
||||
if (this.blockIds[i] === blockIds[j]) {
|
||||
this.blockIds.splice(i, 1)
|
||||
blockIds.splice(j, 1)
|
||||
|
@ -34,33 +34,33 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
const queryString = new URLSearchParams(window.location.search)
|
||||
const queryString = new URLSearchParams(window.location.search)
|
||||
const boardId = queryString.get('id')
|
||||
const viewId = queryString.get('v')
|
||||
|
||||
this.state = {
|
||||
boardId,
|
||||
this.state = {
|
||||
boardId,
|
||||
viewId,
|
||||
workspaceTree: new MutableWorkspaceTree(),
|
||||
}
|
||||
|
||||
Utils.log(`BoardPage. boardId: ${boardId}`)
|
||||
Utils.log(`BoardPage. boardId: ${boardId}`)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||
Utils.log('componentDidUpdate')
|
||||
Utils.log('componentDidUpdate')
|
||||
const board = this.state.boardTree?.board
|
||||
const prevBoard = prevState.boardTree?.board
|
||||
|
||||
const activeView = this.state.boardTree?.activeView
|
||||
const activeView = this.state.boardTree?.activeView
|
||||
const prevActiveView = prevState.boardTree?.activeView
|
||||
|
||||
if (board?.icon !== prevBoard?.icon) {
|
||||
if (board?.icon !== prevBoard?.icon) {
|
||||
Utils.setFavicon(board?.icon)
|
||||
}
|
||||
if (board?.title !== prevBoard?.title || activeView?.title !== prevActiveView?.title) {
|
||||
if (board?.title !== prevBoard?.title || activeView?.title !== prevActiveView?.title) {
|
||||
document.title = `OCTO - ${board?.title} | ${activeView?.title}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
undoRedoHandler = async (e: KeyboardEvent) => {
|
||||
@ -68,53 +68,53 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.keyCode === 90 && !e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Cmd+Z
|
||||
if (e.keyCode === 90 && !e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Cmd+Z
|
||||
Utils.log('Undo')
|
||||
const description = mutator.undoDescription()
|
||||
await mutator.undo()
|
||||
if (description) {
|
||||
FlashMessage.show(`Undo ${description}`)
|
||||
} else {
|
||||
FlashMessage.show('Undo')
|
||||
}
|
||||
} else if (e.keyCode === 90 && e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Shift+Cmd+Z
|
||||
if (description) {
|
||||
FlashMessage.show(`Undo ${description}`)
|
||||
} else {
|
||||
FlashMessage.show('Undo')
|
||||
}
|
||||
} else if (e.keyCode === 90 && e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Shift+Cmd+Z
|
||||
Utils.log('Redo')
|
||||
const description = mutator.redoDescription()
|
||||
const description = mutator.redoDescription()
|
||||
await mutator.redo()
|
||||
if (description) {
|
||||
FlashMessage.show(`Redo ${description}`)
|
||||
} else {
|
||||
FlashMessage.show('Redo')
|
||||
}
|
||||
}
|
||||
FlashMessage.show(`Redo ${description}`)
|
||||
} else {
|
||||
FlashMessage.show('Redo')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.undoRedoHandler)
|
||||
if (this.state.boardId) {
|
||||
if (this.state.boardId) {
|
||||
this.attachToBoard(this.state.boardId, this.state.viewId)
|
||||
} else {
|
||||
} else {
|
||||
this.sync()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
Utils.log(`boardPage.componentWillUnmount: ${this.state.boardId}`)
|
||||
this.workspaceListener.close()
|
||||
document.removeEventListener('keydown', this.undoRedoHandler)
|
||||
document.removeEventListener('keydown', this.undoRedoHandler)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {workspaceTree} = this.state
|
||||
|
||||
if (this.state.filterAnchorElement) {
|
||||
const element = this.state.filterAnchorElement
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const rect = element.getBoundingClientRect()
|
||||
if (this.state.filterAnchorElement) {
|
||||
const element = this.state.filterAnchorElement
|
||||
const bodyRect = document.body.getBoundingClientRect()
|
||||
const rect = element.getBoundingClientRect()
|
||||
|
||||
// Show at bottom-left of element
|
||||
const maxX = bodyRect.right - 420 - 100
|
||||
const pageX = Math.min(maxX, rect.left - bodyRect.left)
|
||||
const pageX = Math.min(maxX, rect.left - bodyRect.left)
|
||||
const pageY = rect.bottom - bodyRect.top
|
||||
|
||||
ReactDOM.render(
|
||||
@ -129,85 +129,85 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
Utils.getElementById('modal'),
|
||||
)
|
||||
} else {
|
||||
const modal = document.getElementById('modal')
|
||||
if (modal) {
|
||||
const modal = document.getElementById('modal')
|
||||
if (modal) {
|
||||
ReactDOM.render(<div/>, modal)
|
||||
}
|
||||
}
|
||||
|
||||
Utils.log(`BoardPage.render ${this.state.boardTree?.board?.title}`)
|
||||
return (
|
||||
<div className='BoardPage'>
|
||||
return (
|
||||
<div className='BoardPage'>
|
||||
<WorkspaceComponent
|
||||
workspaceTree={workspaceTree}
|
||||
boardTree={this.state.boardTree}
|
||||
showView={(id, boardId) => {
|
||||
workspaceTree={workspaceTree}
|
||||
boardTree={this.state.boardTree}
|
||||
showView={(id, boardId) => {
|
||||
this.showView(id, boardId)
|
||||
}}
|
||||
showBoard={(id) => {
|
||||
showBoard={(id) => {
|
||||
this.showBoard(id)
|
||||
}}
|
||||
showFilter={(el) => {
|
||||
showFilter={(el) => {
|
||||
this.showFilter(el)
|
||||
}}
|
||||
setSearchText={(text) => {
|
||||
setSearchText={(text) => {
|
||||
this.setSearchText(text)
|
||||
}}
|
||||
/>
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
private async attachToBoard(boardId: string, viewId?: string) {
|
||||
Utils.log(`attachToBoard: ${boardId}`)
|
||||
this.sync(boardId, viewId)
|
||||
Utils.log(`attachToBoard: ${boardId}`)
|
||||
this.sync(boardId, viewId)
|
||||
}
|
||||
|
||||
async sync(boardId: string = this.state.boardId, viewId: string | undefined = this.state.viewId) {
|
||||
const {workspaceTree} = this.state
|
||||
const {workspaceTree} = this.state
|
||||
Utils.log(`sync start: ${boardId}`)
|
||||
|
||||
await workspaceTree.sync()
|
||||
const boardIds = workspaceTree.boards.map(o => o.id)
|
||||
this.workspaceListener.open(boardIds, async (blockId) => {
|
||||
const boardIds = workspaceTree.boards.map((o) => o.id)
|
||||
this.workspaceListener.open(boardIds, async (blockId) => {
|
||||
Utils.log(`workspaceListener.onChanged: ${blockId}`)
|
||||
this.sync()
|
||||
})
|
||||
|
||||
if (boardId) {
|
||||
const boardTree = new MutableBoardTree(boardId)
|
||||
if (boardId) {
|
||||
const boardTree = new MutableBoardTree(boardId)
|
||||
await boardTree.sync()
|
||||
|
||||
// Default to first view
|
||||
if (!viewId) {
|
||||
viewId = boardTree.views[0].id
|
||||
}
|
||||
// Default to first view
|
||||
if (!viewId) {
|
||||
viewId = boardTree.views[0].id
|
||||
}
|
||||
|
||||
boardTree.setActiveView(viewId)
|
||||
boardTree.setActiveView(viewId)
|
||||
|
||||
// TODO: Handle error (viewId not found)
|
||||
this.setState({
|
||||
...this.state,
|
||||
this.setState({
|
||||
...this.state,
|
||||
boardTree,
|
||||
boardId,
|
||||
viewId: boardTree.activeView.id,
|
||||
boardId,
|
||||
viewId: boardTree.activeView.id,
|
||||
})
|
||||
Utils.log(`sync complete: ${boardTree.board.id} (${boardTree.board.title})`)
|
||||
} else {
|
||||
this.forceUpdate()
|
||||
}
|
||||
} else {
|
||||
this.forceUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
// IPageController
|
||||
showBoard(boardId: string) {
|
||||
const {boardTree} = this.state
|
||||
const {boardTree} = this.state
|
||||
|
||||
if (boardTree?.board?.id === boardId) {
|
||||
if (boardTree?.board?.id === boardId) {
|
||||
return
|
||||
}
|
||||
|
||||
const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}`
|
||||
window.history.pushState({path: newUrl}, '', newUrl)
|
||||
const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}`
|
||||
window.history.pushState({path: newUrl}, '', newUrl)
|
||||
|
||||
this.attachToBoard(boardId)
|
||||
}
|
||||
@ -221,11 +221,11 @@ export default class BoardPage extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
const newUrl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}&v=${encodeURIComponent(viewId)}`
|
||||
window.history.pushState({path: newUrl}, '', newUrl)
|
||||
window.history.pushState({path: newUrl}, '', newUrl)
|
||||
}
|
||||
|
||||
showFilter(anchorElement?: HTMLElement) {
|
||||
this.setState({...this.state, filterAnchorElement: anchorElement})
|
||||
this.setState({...this.state, filterAnchorElement: anchorElement})
|
||||
}
|
||||
|
||||
setSearchText(text?: string) {
|
||||
|
@ -1,13 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react'
|
||||
import { Archiver } from '../archiver'
|
||||
import { MutableBoard } from '../blocks/board'
|
||||
|
||||
import {Archiver} from '../archiver'
|
||||
import {MutableBoard} from '../blocks/board'
|
||||
import Button from '../components/button'
|
||||
import octoClient from '../octoClient'
|
||||
import { IBlock } from '../blocks/block'
|
||||
import { Utils } from '../utils'
|
||||
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {Utils} from '../utils'
|
||||
|
||||
type Props = {}
|
||||
|
||||
@ -28,39 +28,39 @@ export default class HomePage extends React.Component<Props, State> {
|
||||
}
|
||||
|
||||
loadBoards = async () => {
|
||||
const boards = await octoClient.getBlocksWithType('board')
|
||||
const boards = await octoClient.getBlocksWithType('board')
|
||||
this.setState({boards})
|
||||
}
|
||||
|
||||
importClicked = async () => {
|
||||
Archiver.importFullArchive(() => {
|
||||
this.loadBoards()
|
||||
})
|
||||
Archiver.importFullArchive(() => {
|
||||
this.loadBoards()
|
||||
})
|
||||
}
|
||||
|
||||
exportClicked = async () => {
|
||||
Archiver.exportFullArchive()
|
||||
Archiver.exportFullArchive()
|
||||
}
|
||||
|
||||
addClicked = async () => {
|
||||
const board = new MutableBoard()
|
||||
await octoClient.insertBlock(board)
|
||||
const board = new MutableBoard()
|
||||
await octoClient.insertBlock(board)
|
||||
}
|
||||
|
||||
render(): React.ReactNode {
|
||||
return (
|
||||
<div>
|
||||
return (
|
||||
<div>
|
||||
<Button onClick={this.addClicked}>+ Add Board</Button>
|
||||
<br/>
|
||||
<Button onClick={this.addClicked}>Import Archive</Button>
|
||||
<br/>
|
||||
<Button onClick={this.addClicked}>Export Archive</Button>
|
||||
{this.state.boards.map((board) => (
|
||||
<p>
|
||||
<p>
|
||||
<a href={`/board/${board.id}`}>
|
||||
<span>{board.title}</span>
|
||||
<span>{Utils.displayDate(new Date(board.updateAt))}</span>
|
||||
</a>
|
||||
<span>{board.title}</span>
|
||||
<span>{Utils.displayDate(new Date(board.updateAt))}</span>
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
@ -12,280 +12,329 @@ import {Utils} from '../utils'
|
||||
type Group = { option: IPropertyOption, cards: Card[] }
|
||||
|
||||
interface BoardTree {
|
||||
readonly board: Board
|
||||
readonly views: readonly BoardView[]
|
||||
readonly cards: readonly Card[]
|
||||
readonly emptyGroupCards: readonly Card[]
|
||||
readonly groups: readonly Group[]
|
||||
readonly allBlocks: readonly IBlock[]
|
||||
readonly board: Board
|
||||
readonly views: readonly BoardView[]
|
||||
readonly cards: readonly Card[]
|
||||
readonly emptyGroupCards: readonly Card[]
|
||||
readonly groups: readonly Group[]
|
||||
readonly allBlocks: readonly IBlock[]
|
||||
|
||||
readonly activeView?: BoardView
|
||||
readonly groupByProperty?: IPropertyTemplate
|
||||
readonly activeView?: BoardView
|
||||
readonly groupByProperty?: IPropertyTemplate
|
||||
|
||||
getSearchText(): string | undefined
|
||||
getSearchText(): string | undefined
|
||||
}
|
||||
|
||||
class MutableBoardTree implements BoardTree {
|
||||
board!: Board
|
||||
views: MutableBoardView[] = []
|
||||
cards: Card[] = []
|
||||
emptyGroupCards: Card[] = []
|
||||
groups: Group[] = []
|
||||
board!: Board
|
||||
views: MutableBoardView[] = []
|
||||
cards: Card[] = []
|
||||
emptyGroupCards: Card[] = []
|
||||
groups: Group[] = []
|
||||
|
||||
activeView?: MutableBoardView
|
||||
groupByProperty?: IPropertyTemplate
|
||||
activeView?: MutableBoardView
|
||||
groupByProperty?: IPropertyTemplate
|
||||
|
||||
private searchText?: string
|
||||
private allCards: Card[] = []
|
||||
get allBlocks(): IBlock[] {
|
||||
return [this.board, ...this.views, ...this.allCards]
|
||||
}
|
||||
private searchText?: string
|
||||
private allCards: Card[] = []
|
||||
get allBlocks(): IBlock[] {
|
||||
return [this.board, ...this.views, ...this.allCards]
|
||||
}
|
||||
|
||||
constructor(private boardId: string) {
|
||||
}
|
||||
constructor(private boardId: string) {
|
||||
}
|
||||
|
||||
async sync() {
|
||||
const blocks = await octoClient.getSubtree(this.boardId)
|
||||
this.rebuild(OctoUtils.hydrateBlocks(blocks))
|
||||
}
|
||||
async sync() {
|
||||
const blocks = await octoClient.getSubtree(this.boardId)
|
||||
this.rebuild(OctoUtils.hydrateBlocks(blocks))
|
||||
}
|
||||
|
||||
private rebuild(blocks: IBlock[]) {
|
||||
this.board = blocks.find(block => block.type === "board") as Board
|
||||
this.views = blocks.filter(block => block.type === "view") as MutableBoardView[]
|
||||
this.allCards = blocks.filter(block => block.type === "card") as Card[]
|
||||
this.cards = []
|
||||
private rebuild(blocks: IBlock[]) {
|
||||
this.board = blocks.find((block) => block.type === 'board') as Board
|
||||
this.views = blocks.filter((block) => block.type === 'view') as MutableBoardView[]
|
||||
this.allCards = blocks.filter((block) => block.type === 'card') as Card[]
|
||||
this.cards = []
|
||||
|
||||
this.ensureMinimumSchema()
|
||||
}
|
||||
this.ensureMinimumSchema()
|
||||
}
|
||||
|
||||
private async ensureMinimumSchema() {
|
||||
const { board } = this
|
||||
private async ensureMinimumSchema() {
|
||||
const {board} = this
|
||||
|
||||
let didChange = false
|
||||
let didChange = false
|
||||
|
||||
// At least one select property
|
||||
const selectProperties = board.cardProperties.find(o => o.type === "select")
|
||||
if (!selectProperties) {
|
||||
// At least one select property
|
||||
const selectProperties = board.cardProperties.find((o) => o.type === 'select')
|
||||
if (!selectProperties) {
|
||||
const newBoard = new MutableBoard(board)
|
||||
const property: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: "Status",
|
||||
type: "select",
|
||||
options: []
|
||||
}
|
||||
const property: IPropertyTemplate = {
|
||||
id: Utils.createGuid(),
|
||||
name: 'Status',
|
||||
type: 'select',
|
||||
options: [],
|
||||
}
|
||||
newBoard.cardProperties.push(property)
|
||||
this.board = newBoard
|
||||
didChange = true
|
||||
}
|
||||
didChange = true
|
||||
}
|
||||
|
||||
// At least one view
|
||||
if (this.views.length < 1) {
|
||||
const view = new MutableBoardView()
|
||||
view.parentId = board.id
|
||||
view.groupById = board.cardProperties.find(o => o.type === "select")?.id
|
||||
this.views.push(view)
|
||||
didChange = true
|
||||
}
|
||||
// At least one view
|
||||
if (this.views.length < 1) {
|
||||
const view = new MutableBoardView()
|
||||
view.parentId = board.id
|
||||
view.groupById = board.cardProperties.find((o) => o.type === 'select')?.id
|
||||
this.views.push(view)
|
||||
didChange = true
|
||||
}
|
||||
|
||||
return didChange
|
||||
}
|
||||
return didChange
|
||||
}
|
||||
|
||||
setActiveView(viewId: string) {
|
||||
this.activeView = this.views.find(o => o.id === viewId)
|
||||
if (!this.activeView) {
|
||||
Utils.logError(`Cannot find BoardView: ${viewId}`)
|
||||
this.activeView = this.views[0]
|
||||
}
|
||||
setActiveView(viewId: string) {
|
||||
this.activeView = this.views.find((o) => o.id === viewId)
|
||||
if (!this.activeView) {
|
||||
Utils.logError(`Cannot find BoardView: ${viewId}`)
|
||||
this.activeView = this.views[0]
|
||||
}
|
||||
|
||||
// Fix missing group by (e.g. for new views)
|
||||
if (this.activeView.viewType === "board" && !this.activeView.groupById) {
|
||||
this.activeView.groupById = this.board.cardProperties.find(o => o.type === "select")?.id
|
||||
}
|
||||
this.applyFilterSortAndGroup()
|
||||
}
|
||||
// Fix missing group by (e.g. for new views)
|
||||
if (this.activeView.viewType === 'board' && !this.activeView.groupById) {
|
||||
this.activeView.groupById = this.board.cardProperties.find((o) => o.type === 'select')?.id
|
||||
}
|
||||
this.applyFilterSortAndGroup()
|
||||
}
|
||||
|
||||
getSearchText(): string | undefined {
|
||||
return this.searchText
|
||||
}
|
||||
getSearchText(): string | undefined {
|
||||
return this.searchText
|
||||
}
|
||||
|
||||
setSearchText(text?: string) {
|
||||
this.searchText = text
|
||||
this.applyFilterSortAndGroup()
|
||||
}
|
||||
setSearchText(text?: string) {
|
||||
this.searchText = text
|
||||
this.applyFilterSortAndGroup()
|
||||
}
|
||||
|
||||
applyFilterSortAndGroup() {
|
||||
Utils.assert(this.allCards !== undefined)
|
||||
applyFilterSortAndGroup() {
|
||||
Utils.assert(this.allCards !== undefined)
|
||||
|
||||
this.cards = this.filterCards(this.allCards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
this.cards = this.searchFilterCards(this.cards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
this.cards = this.sortCards(this.cards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
this.cards = this.filterCards(this.allCards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
this.cards = this.searchFilterCards(this.cards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
this.cards = this.sortCards(this.cards)
|
||||
Utils.assert(this.cards !== undefined)
|
||||
|
||||
if (this.activeView.groupById) {
|
||||
this.setGroupByProperty(this.activeView.groupById)
|
||||
} else {
|
||||
Utils.assert(this.activeView.viewType !== "board")
|
||||
}
|
||||
if (this.activeView.groupById) {
|
||||
this.setGroupByProperty(this.activeView.groupById)
|
||||
} else {
|
||||
Utils.assert(this.activeView.viewType !== 'board')
|
||||
}
|
||||
|
||||
Utils.assert(this.cards !== undefined)
|
||||
}
|
||||
Utils.assert(this.cards !== undefined)
|
||||
}
|
||||
|
||||
private searchFilterCards(cards: Card[]): Card[] {
|
||||
const searchText = this.searchText?.toLocaleLowerCase()
|
||||
if (!searchText) { return cards.slice() }
|
||||
private searchFilterCards(cards: Card[]): Card[] {
|
||||
const searchText = this.searchText?.toLocaleLowerCase()
|
||||
if (!searchText) {
|
||||
return cards.slice()
|
||||
}
|
||||
|
||||
return cards.filter(card => {
|
||||
if (card.title?.toLocaleLowerCase().indexOf(searchText) !== -1) { return true }
|
||||
})
|
||||
}
|
||||
return cards.filter((card) => {
|
||||
if (card.title?.toLocaleLowerCase().indexOf(searchText) !== -1) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private setGroupByProperty(propertyId: string) {
|
||||
const { board } = this
|
||||
private setGroupByProperty(propertyId: string) {
|
||||
const {board} = this
|
||||
|
||||
let property = board.cardProperties.find(o => o.id === propertyId)
|
||||
// TODO: Handle multi-select
|
||||
if (!property || property.type !== "select") {
|
||||
Utils.logError(`this.view.groupById card property not found: ${propertyId}`)
|
||||
property = board.cardProperties.find(o => o.type === "select")
|
||||
Utils.assertValue(property)
|
||||
}
|
||||
this.groupByProperty = property
|
||||
let property = board.cardProperties.find((o) => o.id === propertyId)
|
||||
|
||||
this.groupCards()
|
||||
}
|
||||
// TODO: Handle multi-select
|
||||
if (!property || property.type !== 'select') {
|
||||
Utils.logError(`this.view.groupById card property not found: ${propertyId}`)
|
||||
property = board.cardProperties.find((o) => o.type === 'select')
|
||||
Utils.assertValue(property)
|
||||
}
|
||||
this.groupByProperty = property
|
||||
|
||||
private groupCards() {
|
||||
this.groups = []
|
||||
this.groupCards()
|
||||
}
|
||||
|
||||
const groupByPropertyId = this.groupByProperty.id
|
||||
private groupCards() {
|
||||
this.groups = []
|
||||
|
||||
this.emptyGroupCards = this.cards.filter(o => {
|
||||
const propertyValue = o.properties[groupByPropertyId]
|
||||
return !propertyValue || !this.groupByProperty.options.find(option => option.value === propertyValue)
|
||||
})
|
||||
const groupByPropertyId = this.groupByProperty.id
|
||||
|
||||
const propertyOptions = this.groupByProperty.options || []
|
||||
for (const option of propertyOptions) {
|
||||
const cards = this.cards
|
||||
.filter(o => {
|
||||
const propertyValue = o.properties[groupByPropertyId]
|
||||
return propertyValue && propertyValue === option.value
|
||||
})
|
||||
this.emptyGroupCards = this.cards.filter((o) => {
|
||||
const propertyValue = o.properties[groupByPropertyId]
|
||||
return !propertyValue || !this.groupByProperty.options.find((option) => option.value === propertyValue)
|
||||
})
|
||||
|
||||
const group: Group = {
|
||||
option,
|
||||
cards
|
||||
}
|
||||
const propertyOptions = this.groupByProperty.options || []
|
||||
for (const option of propertyOptions) {
|
||||
const cards = this.cards.
|
||||
filter((o) => {
|
||||
const propertyValue = o.properties[groupByPropertyId]
|
||||
return propertyValue && propertyValue === option.value
|
||||
})
|
||||
|
||||
this.groups.push(group)
|
||||
}
|
||||
}
|
||||
const group: Group = {
|
||||
option,
|
||||
cards,
|
||||
}
|
||||
|
||||
private filterCards(cards: Card[]): Card[] {
|
||||
const { board } = this
|
||||
const filterGroup = this.activeView?.filter
|
||||
if (!filterGroup) { return cards.slice() }
|
||||
this.groups.push(group)
|
||||
}
|
||||
}
|
||||
|
||||
return CardFilter.applyFilterGroup(filterGroup, board.cardProperties, cards)
|
||||
}
|
||||
private filterCards(cards: Card[]): Card[] {
|
||||
const {board} = this
|
||||
const filterGroup = this.activeView?.filter
|
||||
if (!filterGroup) {
|
||||
return cards.slice()
|
||||
}
|
||||
|
||||
private sortCards(cards: Card[]): Card[] {
|
||||
if (!this.activeView) { Utils.assertFailure(); return cards }
|
||||
const { board } = this
|
||||
const { sortOptions } = this.activeView
|
||||
let sortedCards: Card[] = []
|
||||
return CardFilter.applyFilterGroup(filterGroup, board.cardProperties, cards)
|
||||
}
|
||||
|
||||
if (sortOptions.length < 1) {
|
||||
Utils.log(`Default sort`)
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
const aValue = a.title || ""
|
||||
const bValue = b.title || ""
|
||||
private sortCards(cards: Card[]): Card[] {
|
||||
if (!this.activeView) {
|
||||
Utils.assertFailure(); return cards
|
||||
}
|
||||
const {board} = this
|
||||
const {sortOptions} = this.activeView
|
||||
let sortedCards: Card[] = []
|
||||
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) { return -1 }
|
||||
if (bValue && !aValue) { return 1 }
|
||||
if (!aValue && !bValue) { return a.createAt - b.createAt }
|
||||
if (sortOptions.length < 1) {
|
||||
Utils.log('Default sort')
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
const aValue = a.title || ''
|
||||
const bValue = b.title || ''
|
||||
|
||||
return a.createAt - b.createAt
|
||||
})
|
||||
} else {
|
||||
sortOptions.forEach(sortOption => {
|
||||
if (sortOption.propertyId === "__name") {
|
||||
Utils.log(`Sort by name`)
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
const aValue = a.title || ""
|
||||
const bValue = b.title || ""
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) {
|
||||
return -1
|
||||
}
|
||||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
// Always put empty values at the bottom, newest last
|
||||
if (aValue && !bValue) { return -1 }
|
||||
if (bValue && !aValue) { return 1 }
|
||||
if (!aValue && !bValue) { return a.createAt - b.createAt }
|
||||
return a.createAt - b.createAt
|
||||
})
|
||||
} else {
|
||||
sortOptions.forEach((sortOption) => {
|
||||
if (sortOption.propertyId === '__name') {
|
||||
Utils.log('Sort by name')
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
const aValue = a.title || ''
|
||||
const bValue = b.title || ''
|
||||
|
||||
let result = aValue.localeCompare(bValue)
|
||||
if (sortOption.reversed) { result = -result }
|
||||
return result
|
||||
})
|
||||
} else {
|
||||
const sortPropertyId = sortOption.propertyId
|
||||
const template = board.cardProperties.find(o => o.id === sortPropertyId)
|
||||
if (!template) {
|
||||
Utils.logError(`Missing template for property id: ${sortPropertyId}`)
|
||||
return cards.slice()
|
||||
}
|
||||
Utils.log(`Sort by ${template?.name}`)
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
// Always put cards with no titles at the bottom
|
||||
if (a.title && !b.title) { return -1 }
|
||||
if (b.title && !a.title) { return 1 }
|
||||
if (!a.title && !b.title) { return a.createAt - b.createAt }
|
||||
// Always put empty values at the bottom, newest last
|
||||
if (aValue && !bValue) {
|
||||
return -1
|
||||
}
|
||||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
const aValue = a.properties[sortPropertyId] || ""
|
||||
const bValue = b.properties[sortPropertyId] || ""
|
||||
let result = 0
|
||||
if (template.type === "select") {
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) { return -1 }
|
||||
if (bValue && !aValue) { return 1 }
|
||||
if (!aValue && !bValue) { return a.createAt - b.createAt }
|
||||
let result = aValue.localeCompare(bValue)
|
||||
if (sortOption.reversed) {
|
||||
result = -result
|
||||
}
|
||||
return result
|
||||
})
|
||||
} else {
|
||||
const sortPropertyId = sortOption.propertyId
|
||||
const template = board.cardProperties.find((o) => o.id === sortPropertyId)
|
||||
if (!template) {
|
||||
Utils.logError(`Missing template for property id: ${sortPropertyId}`)
|
||||
return cards.slice()
|
||||
}
|
||||
Utils.log(`Sort by ${template?.name}`)
|
||||
sortedCards = cards.sort((a, b) => {
|
||||
// Always put cards with no titles at the bottom
|
||||
if (a.title && !b.title) {
|
||||
return -1
|
||||
}
|
||||
if (b.title && !a.title) {
|
||||
return 1
|
||||
}
|
||||
if (!a.title && !b.title) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
// Sort by the option order (not alphabetically by value)
|
||||
const aOrder = template.options.findIndex(o => o.value === aValue)
|
||||
const bOrder = template.options.findIndex(o => o.value === bValue)
|
||||
const aValue = a.properties[sortPropertyId] || ''
|
||||
const bValue = b.properties[sortPropertyId] || ''
|
||||
let result = 0
|
||||
if (template.type === 'select') {
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) {
|
||||
return -1
|
||||
}
|
||||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
result = aOrder - bOrder
|
||||
} else if (template.type === "number" || template.type === "date") {
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) { return -1 }
|
||||
if (bValue && !aValue) { return 1 }
|
||||
if (!aValue && !bValue) { return a.createAt - b.createAt }
|
||||
// Sort by the option order (not alphabetically by value)
|
||||
const aOrder = template.options.findIndex((o) => o.value === aValue)
|
||||
const bOrder = template.options.findIndex((o) => o.value === bValue)
|
||||
|
||||
result = Number(aValue) - Number(bValue)
|
||||
} else if (template.type === "createdTime") {
|
||||
result = a.createAt - b.createAt
|
||||
} else if (template.type === "updatedTime") {
|
||||
result = a.updateAt - b.updateAt
|
||||
} else {
|
||||
// Text-based sort
|
||||
result = aOrder - bOrder
|
||||
} else if (template.type === 'number' || template.type === 'date') {
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) {
|
||||
return -1
|
||||
}
|
||||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) { return -1 }
|
||||
if (bValue && !aValue) { return 1 }
|
||||
if (!aValue && !bValue) { return a.createAt - b.createAt }
|
||||
result = Number(aValue) - Number(bValue)
|
||||
} else if (template.type === 'createdTime') {
|
||||
result = a.createAt - b.createAt
|
||||
} else if (template.type === 'updatedTime') {
|
||||
result = a.updateAt - b.updateAt
|
||||
} else {
|
||||
// Text-based sort
|
||||
|
||||
result = aValue.localeCompare(bValue)
|
||||
}
|
||||
// Always put empty values at the bottom
|
||||
if (aValue && !bValue) {
|
||||
return -1
|
||||
}
|
||||
if (bValue && !aValue) {
|
||||
return 1
|
||||
}
|
||||
if (!aValue && !bValue) {
|
||||
return a.createAt - b.createAt
|
||||
}
|
||||
|
||||
if (sortOption.reversed) { result = -result }
|
||||
return result
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
result = aValue.localeCompare(bValue)
|
||||
}
|
||||
|
||||
return sortedCards
|
||||
}
|
||||
if (sortOption.reversed) {
|
||||
result = -result
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return sortedCards
|
||||
}
|
||||
}
|
||||
|
||||
export { MutableBoardTree, BoardTree }
|
||||
export {MutableBoardTree, BoardTree}
|
||||
|
@ -1,40 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {Card} from '../blocks/card'
|
||||
import { IOrderedBlock } from '../blocks/orderedBlock'
|
||||
import {IOrderedBlock} from '../blocks/orderedBlock'
|
||||
import octoClient from '../octoClient'
|
||||
import {IBlock} from '../blocks/block'
|
||||
import {OctoUtils} from '../octoUtils'
|
||||
|
||||
interface CardTree {
|
||||
readonly card: Card
|
||||
readonly comments: readonly IBlock[]
|
||||
readonly contents: readonly IOrderedBlock[]
|
||||
readonly card: Card
|
||||
readonly comments: readonly IBlock[]
|
||||
readonly contents: readonly IOrderedBlock[]
|
||||
}
|
||||
|
||||
class MutableCardTree implements CardTree {
|
||||
card: Card
|
||||
comments: IBlock[]
|
||||
contents: IOrderedBlock[]
|
||||
card: Card
|
||||
comments: IBlock[]
|
||||
contents: IOrderedBlock[]
|
||||
|
||||
constructor(private cardId: string) {
|
||||
}
|
||||
constructor(private cardId: string) {
|
||||
}
|
||||
|
||||
async sync() {
|
||||
const blocks = await octoClient.getSubtree(this.cardId)
|
||||
this.rebuild(OctoUtils.hydrateBlocks(blocks))
|
||||
}
|
||||
async sync() {
|
||||
const blocks = await octoClient.getSubtree(this.cardId)
|
||||
this.rebuild(OctoUtils.hydrateBlocks(blocks))
|
||||
}
|
||||
|
||||
private rebuild(blocks: IBlock[]) {
|
||||
this.card = blocks.find(o => o.id === this.cardId) as Card
|
||||
private rebuild(blocks: IBlock[]) {
|
||||
this.card = blocks.find((o) => o.id === this.cardId) as Card
|
||||
|
||||
this.comments = blocks
|
||||
.filter(block => block.type === "comment")
|
||||
.sort((a, b) => a.createAt - b.createAt)
|
||||
this.comments = blocks.
|
||||
filter((block) => block.type === 'comment').
|
||||
sort((a, b) => a.createAt - b.createAt)
|
||||
|
||||
const contentBlocks = blocks.filter(block => block.type === "text" || block.type === "image") as IOrderedBlock[]
|
||||
this.contents = contentBlocks.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
const contentBlocks = blocks.filter((block) => block.type === 'text' || block.type === 'image') as IOrderedBlock[]
|
||||
this.contents = contentBlocks.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
}
|
||||
|
||||
export { MutableCardTree, CardTree }
|
||||
export {MutableCardTree, CardTree}
|
||||
|
@ -7,29 +7,29 @@ import {OctoUtils} from '../octoUtils'
|
||||
import {BoardView} from '../blocks/boardView'
|
||||
|
||||
interface WorkspaceTree {
|
||||
readonly boards: readonly Board[]
|
||||
readonly views: readonly BoardView[]
|
||||
readonly boards: readonly Board[]
|
||||
readonly views: readonly BoardView[]
|
||||
}
|
||||
|
||||
class MutableWorkspaceTree {
|
||||
boards: Board[] = []
|
||||
views: BoardView[] = []
|
||||
boards: Board[] = []
|
||||
views: BoardView[] = []
|
||||
|
||||
async sync() {
|
||||
const boards = await octoClient.getBlocksWithType("board")
|
||||
const views = await octoClient.getBlocksWithType("view")
|
||||
this.rebuild(
|
||||
async sync() {
|
||||
const boards = await octoClient.getBlocksWithType('board')
|
||||
const views = await octoClient.getBlocksWithType('view')
|
||||
this.rebuild(
|
||||
OctoUtils.hydrateBlocks(boards),
|
||||
OctoUtils.hydrateBlocks(views)
|
||||
OctoUtils.hydrateBlocks(views),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private rebuild(boards: IBlock[], views: IBlock[]) {
|
||||
this.boards = boards.filter(block => block.type === "board") as Board[]
|
||||
this.views = views.filter(block => block.type === "view") as BoardView[]
|
||||
}
|
||||
private rebuild(boards: IBlock[], views: IBlock[]) {
|
||||
this.boards = boards.filter((block) => block.type === 'board') as Board[]
|
||||
this.views = views.filter((block) => block.type === 'view') as BoardView[]
|
||||
}
|
||||
}
|
||||
|
||||
// type WorkspaceTree = Readonly<MutableWorkspaceTree>
|
||||
|
||||
export { MutableWorkspaceTree, WorkspaceTree }
|
||||
export {MutableWorkspaceTree, WorkspaceTree}
|
||||
|
Loading…
Reference in New Issue
Block a user