1
0
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:
Chen-I Lim 2020-10-21 15:03:12 -07:00
parent 262f3c043d
commit a8a274ff0f
27 changed files with 999 additions and 932 deletions

View File

@ -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'

View File

@ -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'

View File

@ -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 = {}) {

View File

@ -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

View File

@ -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

View File

@ -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 = {}) {

View File

@ -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

View File

@ -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)
}

View File

@ -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

View File

@ -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>)

View File

@ -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,
)
)
}
}

View File

@ -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}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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() {

View File

@ -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

View File

@ -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)
)
}
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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) {

View File

@ -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>

View File

@ -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}

View File

@ -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}

View File

@ -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}