diff --git a/webapp/src/blocks/board.ts b/webapp/src/blocks/board.ts index 1d57b4e9a..8c880caa6 100644 --- a/webapp/src/blocks/board.ts +++ b/webapp/src/blocks/board.ts @@ -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' diff --git a/webapp/src/blocks/boardView.ts b/webapp/src/blocks/boardView.ts index 477141e56..ca7fe9e64 100644 --- a/webapp/src/blocks/boardView.ts +++ b/webapp/src/blocks/boardView.ts @@ -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' diff --git a/webapp/src/blocks/commentBlock.ts b/webapp/src/blocks/commentBlock.ts index 175f49dc9..93abbef88 100644 --- a/webapp/src/blocks/commentBlock.ts +++ b/webapp/src/blocks/commentBlock.ts @@ -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 = {}) { diff --git a/webapp/src/blocks/imageBlock.ts b/webapp/src/blocks/imageBlock.ts index 41b989632..da69c2bf4 100644 --- a/webapp/src/blocks/imageBlock.ts +++ b/webapp/src/blocks/imageBlock.ts @@ -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 diff --git a/webapp/src/blocks/orderedBlock.ts b/webapp/src/blocks/orderedBlock.ts index cfcd99707..346a5b60f 100644 --- a/webapp/src/blocks/orderedBlock.ts +++ b/webapp/src/blocks/orderedBlock.ts @@ -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 diff --git a/webapp/src/blocks/textBlock.ts b/webapp/src/blocks/textBlock.ts index a1a671788..7a5b647c1 100644 --- a/webapp/src/blocks/textBlock.ts +++ b/webapp/src/blocks/textBlock.ts @@ -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 = {}) { diff --git a/webapp/src/components/boardCard.tsx b/webapp/src/components/boardCard.tsx index e510656d9..333da4049 100644 --- a/webapp/src/components/boardCard.tsx +++ b/webapp/src/components/boardCard.tsx @@ -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 {
{card.title || 'Untitled'}
{visiblePropertyTemplates.map((template) => { - return OctoUtils.propertyValueReadonlyElement(card, template, '') - })} + return OctoUtils.propertyValueReadonlyElement(card, template, '') + })} ) return element diff --git a/webapp/src/components/cardDetail.tsx b/webapp/src/components/cardDetail.tsx index bb056ab44..e63e45670 100644 --- a/webapp/src/components/cardDetail.tsx +++ b/webapp/src/components/cardDetail.tsx @@ -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 { 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 { } 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() + const backgroundRef = React.createRef() const newCommentRef = React.createRef() const sendCommentButtonRef = React.createRef() let contentElements @@ -81,14 +81,14 @@ export default class CardDetail extends React.Component { contentElements = (
{cardTree.contents.map((block) => { - if (block.type === 'text') { - const cardText = block.title - return (
+ >
-
{ this.showContentBlockMenu(e, block) @@ -96,22 +96,24 @@ export default class CardDetail extends React.Component { >
-
+
{ + text={cardText} + placeholderText='Edit text...' + onChanged={(text) => { Utils.log(`change text ${block.id}, ${text}`) mutator.changeTitle(block, text, 'edit card text') }} - /> + />
) - } else if (block.type === 'image') { - const url = block.fields.url - return (
+ >
-
{ this.showContentBlockMenu(e, block) @@ -119,18 +121,18 @@ export default class CardDetail extends React.Component { >
-
- + {block.title} + />
) - } + } - return
- })} + return
+ })}
) - } else { + } else { contentElements = (
@@ -147,58 +149,58 @@ export default class CardDetail extends React.Component { />
) - } + } const icon = card.icon - // TODO: Replace this placeholder + // TODO: Replace this placeholder const username = 'John Smith' const userImageUrl = 'data:image/svg+xml,' - return ( + return ( <> -
+
{icon ? -
{ this.iconClicked(e) }} >{icon}
: undefined - } + }
{ + className='octo-hovercontrols' + onMouseOver={() => { this.setState({...this.state, isHoverOnCover: true}) }} - onMouseLeave={() => { + onMouseLeave={() => { this.setState({...this.state, isHoverOnCover: false}) }} - > - -
+
{ + ref={this.titleRef} + className='title' + text={card.title} + placeholderText='Untitled' + onChanged={(text) => { mutator.changeTitle(card, text) }} - /> + /> {/* Property list */}
- {board.cardProperties.map((propertyTemplate) => { + {board.cardProperties.map((propertyTemplate) => { return (
{ 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}
{OctoUtils.propertyValueEditableElement(card, propertyTemplate)}
- ) + ) })} -
{ // TODO: Show UI - await mutator.insertPropertyTemplate(boardTree) - }} + await mutator.insertPropertyTemplate(boardTree) + }} >+ Add a property
-
+
{/* Comments */}
- {comments.map((comment) => { + {comments.map((comment) => { const optionsButtonRef = React.createRef() 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 {
{comment.title}
) - })} + })} - {/* New comment */} + {/* New comment */} -
+
+ className='comment-avatar' + src={userImageUrl} + /> { }} - 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() + } + }} + />
{ - 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
+ >Send
-
+

- {/* Content blocks */} + {/* Content blocks */} -
+
{contentElements}
-
+
-
{ - 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
-
+
- + ) } @@ -398,56 +400,56 @@ export default class CardDetail extends React.Component { 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 { 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 { 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) } diff --git a/webapp/src/components/editable.tsx b/webapp/src/components/editable.tsx index 57eeadc53..5b3818652 100644 --- a/webapp/src/components/editable.tsx +++ b/webapp/src/components/editable.tsx @@ -24,7 +24,7 @@ type State = { class Editable extends React.Component { static defaultProps = { text: '', - isMarkdown: false, + isMarkdown: false, isMultiline: false, } @@ -36,30 +36,30 @@ class Editable extends React.Component { 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() 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 { } 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 { 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 diff --git a/webapp/src/components/markdownEditor.tsx b/webapp/src/components/markdownEditor.tsx index 2b84eab2b..7cf479bb7 100644 --- a/webapp/src/components/markdownEditor.tsx +++ b/webapp/src/components/markdownEditor.tsx @@ -38,30 +38,30 @@ class MarkdownEditor extends React.Component { private previewRef = React.createRef() 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 { let html: string if (text) { - html = Utils.htmlFromMarkdown(text) - } else { + html = Utils.htmlFromMarkdown(text) + } else { html = Utils.htmlFromMarkdown(placeholderText || '') } - const previewElement = + const previewElement = (
{ - if (!isEditing) { - this.showEditor() - } - }} + if (!isEditing) { + this.showEditor() + } + }} />); const editorElement = @@ -96,75 +96,75 @@ class MarkdownEditor extends React.Component { // 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() + } + }} > { - 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, + }, + }} />
) diff --git a/webapp/src/components/rootPortal.tsx b/webapp/src/components/rootPortal.tsx index 8640ad648..3aef91865 100644 --- a/webapp/src/components/rootPortal.tsx +++ b/webapp/src/components/rootPortal.tsx @@ -13,11 +13,11 @@ export default class RootPortal extends React.PureComponent { 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 { 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, - ) + ) } } diff --git a/webapp/src/components/sidebar.tsx b/webapp/src/components/sidebar.tsx index a5beca55c..4b1f2cfbc 100644 --- a/webapp/src/components/sidebar.tsx +++ b/webapp/src/components/sidebar.tsx @@ -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 { { 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 (
-
{ this.boardClicked(board) }}> +
{ + this.boardClicked(board) + }} + > {board.icon ? `${board.icon} ${displayTitle}` : displayTitle}
@@ -63,12 +68,20 @@ class Sidebar extends React.Component {
- {boardViews.map(view => { - return
-
{ this.viewClicked(board, view) }}> + {boardViews.map((view) => { + return (
+
{ + this.viewClicked(board, view) + }} + > {view.title || '(Untitled View)'}
-
+
) })}
) @@ -136,4 +149,4 @@ class Sidebar extends React.Component { } } -export { Sidebar } +export {Sidebar} diff --git a/webapp/src/components/switch.tsx b/webapp/src/components/switch.tsx index 5d72818da..47e67c58c 100644 --- a/webapp/src/components/switch.tsx +++ b/webapp/src/components/switch.tsx @@ -24,23 +24,23 @@ class Switch extends React.Component { 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 = (
{ } 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) } } diff --git a/webapp/src/components/tableComponent.tsx b/webapp/src/components/tableComponent.tsx index 185925d9e..33161bde4 100644 --- a/webapp/src/components/tableComponent.tsx +++ b/webapp/src/components/tableComponent.tsx @@ -48,26 +48,26 @@ class TableComponent extends React.Component { } 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 (
Loading...
- ) - } + ) + } - 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 (
@@ -92,7 +92,7 @@ class TableComponent extends React.Component { @@ -268,19 +268,19 @@ class TableComponent extends React.Component { const openButonRef = React.createRef() const tableRowRef = React.createRef() - let focusOnMount = false + let focusOnMount = false if (this.cardIdToFocusOnRender && this.cardIdToFocusOnRender === card.id) { - this.cardIdToFocusOnRender = undefined + this.cardIdToFocusOnRender = undefined focusOnMount = true - } + } - const tableRow = ( { + const tableRow = ( { 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 { } } }} - />) + />) this.cardIdToRowMap.set(card.id, tableRowRef) @@ -311,7 +311,7 @@ class TableComponent extends React.Component {
- ) + ) } private async propertiesClicked(e: React.MouseEvent) { @@ -319,25 +319,25 @@ class TableComponent extends React.Component { 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 { 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 { private async headerClicked(e: React.MouseEvent, 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 { } 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 { } searchChanged(text?: string) { - this.props.setSearchText(text) + this.props.setSearchText(text) } } diff --git a/webapp/src/components/tableRow.tsx b/webapp/src/components/tableRow.tsx index cd1b1dab2..fe1936739 100644 --- a/webapp/src/components/tableRow.tsx +++ b/webapp/src/components/tableRow.tsx @@ -30,12 +30,12 @@ class TableRow extends React.Component { 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() @@ -104,7 +104,7 @@ class TableRow extends React.Component { })}
) - return element + return element } focusOnTitle() { diff --git a/webapp/src/components/viewMenu.tsx b/webapp/src/components/viewMenu.tsx index 701c0c236..c69eecde2 100644 --- a/webapp/src/components/viewMenu.tsx +++ b/webapp/src/components/viewMenu.tsx @@ -17,38 +17,38 @@ type Props = { export default class ViewMenu extends React.Component { 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 { 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 { } render() { - const {boardTree} = this.props + const {boardTree} = this.props return ( {boardTree.views.map((view) => ( { if (FilterGroup.isAnInstanceOf(p)) { return new FilterGroup(p) diff --git a/webapp/src/menu.ts b/webapp/src/menu.ts index 786adcec3..e2ccdd6d3 100644 --- a/webapp/src/menu.ts +++ b/webapp/src/menu.ts @@ -37,13 +37,13 @@ class Menu { const menuElement = menu.appendChild(Utils.htmlToElement('')) 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('')) + for (const option of this.options) { + if (option.type === 'separator') { + const optionElement = menuElement.appendChild(Utils.htmlToElement('')) } else { const optionElement = menuElement.appendChild(Utils.htmlToElement('')) optionElement.id = option.id @@ -53,62 +53,62 @@ class Menu { optionElement.appendChild(Utils.htmlToElement('
')) 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(`
`)) } - } + if (iconName) { + optionElement.appendChild(Utils.htmlToElement(`
`)) + } + } - 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(''), 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(`
`)) - switchElement.appendChild(Utils.htmlToElement('
')) - switchElement.onclick = (e) => { + const className = option.isOn ? 'octo-switch on' : 'octo-switch' + const switchElement = optionElement.appendChild(Utils.htmlToElement(`
`)) + switchElement.appendChild(Utils.htmlToElement('
')) + 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) } diff --git a/webapp/src/mutator.ts b/webapp/src/mutator.ts index 5801184a8..0146571ad 100644 --- a/webapp/src/mutator.ts +++ b/webapp/src/mutator.ts @@ -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) diff --git a/webapp/src/octoClient.ts b/webapp/src/octoClient.ts index fb2b5def4..4b9cb5091 100644 --- a/webapp/src/octoClient.ts +++ b/webapp/src/octoClient.ts @@ -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 { - 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 { 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 { 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 { 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 { - 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 { - 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 { - return this.insertBlocks([block]) + return this.insertBlocks([block]) } async insertBlocks(blocks: IBlock[]): Promise { - 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 { // 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 diff --git a/webapp/src/octoListener.ts b/webapp/src/octoListener.ts index 6a9297037..d6644ef51 100644 --- a/webapp/src/octoListener.ts +++ b/webapp/src/octoListener.ts @@ -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 { 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 { 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 { Utils.getElementById('modal'), ) } else { - const modal = document.getElementById('modal') - if (modal) { + const modal = document.getElementById('modal') + if (modal) { ReactDOM.render(
, modal) } } Utils.log(`BoardPage.render ${this.state.boardTree?.board?.title}`) - return ( -
+ return ( +
{ + 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) }} - /> + />
) } 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 { } 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) { diff --git a/webapp/src/pages/homePage.tsx b/webapp/src/pages/homePage.tsx index da961b941..146afbf92 100644 --- a/webapp/src/pages/homePage.tsx +++ b/webapp/src/pages/homePage.tsx @@ -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 { } 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 ( -
+ return ( +


{this.state.boards.map((board) => ( -

+

- {board.title} - {Utils.displayDate(new Date(board.updateAt))} - + {board.title} + {Utils.displayDate(new Date(board.updateAt))} +

))}
diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index fe679ba8c..053912106 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -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} diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 6507de284..c962bcdd7 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -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} diff --git a/webapp/src/viewModel/workspaceTree.ts b/webapp/src/viewModel/workspaceTree.ts index c588e3aa6..cb8f595a4 100644 --- a/webapp/src/viewModel/workspaceTree.ts +++ b/webapp/src/viewModel/workspaceTree.ts @@ -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 -export { MutableWorkspaceTree, WorkspaceTree } +export {MutableWorkspaceTree, WorkspaceTree}