diff --git a/src/client/components/cardDetail.tsx b/src/client/components/cardDetail.tsx new file mode 100644 index 000000000..17f5cb657 --- /dev/null +++ b/src/client/components/cardDetail.tsx @@ -0,0 +1,415 @@ +import React from "react" +import { BlockIcons } from "../blockIcons" +import { Block } from "../blocks/block" +import { Card } from "../blocks/card" +import { TextBlock } from "../blocks/textBlock" +import { BoardTree } from "../boardTree" +import { CardTree } from "../cardTree" +import { Menu as OldMenu, MenuOption } from "../menu" +import mutator from "../mutator" +import { OctoListener } from "../octoListener" +import { IBlock, IOrderedBlock } from "../octoTypes" +import { OctoUtils } from "../octoUtils" +import { PropertyMenu } from "../propertyMenu" +import { Utils } from "../utils" +import Button from "./button" +import { Editable } from "./editable" +import { MarkdownEditor } from "./markdownEditor" + +type Props = { + boardTree: BoardTree + card: Card +} + +type State = { + isHoverOnCover: boolean + cardTree?: CardTree +} + +export default class CardDetail extends React.Component { + private titleRef = React.createRef() + private keydownHandler: any + private cardListener: OctoListener + + constructor(props: Props) { + super(props) + this.state = { isHoverOnCover: false } + } + + componentDidMount() { + this.cardListener = new OctoListener() + this.cardListener.open(this.props.card.id, async () => { + await cardTree.sync() + this.setState({ cardTree }) + }) + const cardTree = new CardTree(this.props.card.id) + cardTree.sync().then(() => { + this.setState({cardTree}) + setTimeout(() => { + if (this.titleRef.current) { + this.titleRef.current.focus() + } + }, 0) + }) + + } + + render() { + const { boardTree, card } = this.props + const { cardTree } = this.state + const { board } = boardTree + if (!cardTree) { + return null + } + const { comments } = cardTree + + const newCommentPlaceholderText = "Add a comment..." + + const backgroundRef = React.createRef() + const newCommentRef = React.createRef() + const sendCommentButtonRef = React.createRef() + let contentElements + if (cardTree.contents.length > 0) { + contentElements = +
+ {cardTree.contents.map(block => { + if (block.type === "text") { + const cardText = block.title + return
+
+
{ this.showContentBlockMenu(e, block) }}> +
+
+
+ { + 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) }}> +
+
+
+ {block.title} +
+ } + + return
+ })} +
+ } else { + contentElements =
+
+
+ { + const order = cardTree.contents.length * 1000 + const block = new Block({ type: "text", parentId: card.id, title: text, order }) + mutator.insertBlock(block, "add card text") + }} /> +
+
+ } + + const icon = card.icon + + // TODO: Replace this placeholder + const username = "John Smith" + const userImageUrl = `data:image/svg+xml,` + + return ( + <> +
+ {icon ? +
{ this.iconClicked(e) }}>{icon}
+ : undefined + } +
{ this.setState({ ...this.state, isHoverOnCover: true }) }} + onMouseLeave={() => { this.setState({ ...this.state, isHoverOnCover: false }) }} + > + +
+ + { mutator.changeTitle(card, text) }} /> + + {/* Property list */} + +
+ {board.cardProperties.map(propertyTemplate => { + return ( +
+
{ + const menu = PropertyMenu.shared + menu.property = propertyTemplate + menu.onNameChanged = (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") + break + case "type-updatedTime": + await mutator.changePropertyType(board, propertyTemplate, "updatedTime") + break + case "type-select": + await mutator.changePropertyType(board, propertyTemplate, "select") + break + case "delete": + await mutator.deleteProperty(boardTree, propertyTemplate.id) + break + 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) + }}>+ Add a property
+
+ + {/* Comments */} + +
+
+ {comments.map(comment => { + const optionsButtonRef = React.createRef() + const showCommentMenu = (e: React.MouseEvent, activeComment: IBlock) => { + OldMenu.shared.options = [ + { id: "delete", name: "Delete" } + ] + OldMenu.shared.onMenuClicked = (id) => { + switch (id) { + case "delete": { + mutator.deleteBlock(activeComment) + break + } + } + } + OldMenu.shared.showAtElement(e.target as HTMLElement) + } + + return
{ optionsButtonRef.current.style.display = null }} onMouseLeave={() => { optionsButtonRef.current.style.display = "none" }}> +
+ +
{username}
+
{(new Date(comment.createAt)).toLocaleTimeString()}
+
{ showCommentMenu(e, comment) }}>...
+
+
{comment.title}
+
+ })} + + {/* New comment */} + +
+ + { return }} + onFocus={() => { + sendCommentButtonRef.current.style.display = null + }} + 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() + } + }} + > + +
{ + const text = newCommentRef.current.text + console.log(`Send comment: ${newCommentRef.current.text}`) + this.sendComment(text) + newCommentRef.current.text = undefined + newCommentRef.current.blur() + }} + >Send
+
+
+ +
+
+ + {/* Content blocks */} + +
+ {contentElements} +
+ +
+
+
{ + OldMenu.shared.options = [ + { id: "text", name: "Text" }, + { id: "image", name: "Image" }, + ] + OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => { + switch (optionId) { + case "text": + const order = cardTree.contents.length * 1000 + const block = new Block({ type: "text", parentId: card.id, order }) + await mutator.insertBlock(block, "add text") + break + case "image": + Utils.selectLocalFile( + (file) => { + mutator.createImageBlock(card.id, file, cardTree.contents.length * 1000) + }, + ".jpg,.jpeg,.png") + break + } + } + OldMenu.shared.showAtElement(e.target as HTMLElement) + }} + >Add content
+
+
+ + ) + } + + async sendComment(text: string) { + const { card } = this.props + + Utils.assertValue(card) + + const block = new Block({ type: "comment", parentId: card.id, title: text }) + await mutator.insertBlock(block, "add comment") + } + + private showContentBlockMenu(e: React.MouseEvent, block: IOrderedBlock) { + const { cardTree } = this.state + const { card } = this.props + const index = cardTree.contents.indexOf(block) + + const options: MenuOption[] = [] + if (index > 0) { + options.push({ id: "moveUp", name: "Move up" }) + } + if (index < cardTree.contents.length - 1) { + options.push({ id: "moveDown", name: "Move down" }) + } + + options.push( + { id: "insertAbove", name: "Insert above", type: "submenu" }, + { id: "delete", name: "Delete" } + ) + + OldMenu.shared.options = options + OldMenu.shared.subMenuOptions.set("insertAbove", [ + { id: "text", name: "Text" }, + { id: "image", name: "Image" }, + ]) + OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => { + 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) + Utils.log(`moveUp ${newOrder}`) + mutator.changeOrder(block, newOrder, "move up") + break + } + 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) + Utils.log(`moveDown ${newOrder}`) + mutator.changeOrder(block, newOrder, "move down") + break + } + case "insertAbove-text": { + const newBlock = new TextBlock({ parentId: card.id }) + // TODO: Handle need to reorder all blocks + newBlock.order = OctoUtils.getOrderBefore(block, cardTree.contents) + Utils.log(`insert block ${block.id}, order: ${block.order}`) + mutator.insertBlock(newBlock, "insert card text") + break + } + case "insertAbove-image": { + Utils.selectLocalFile( + (file) => { + mutator.createImageBlock(card.id, file, OctoUtils.getOrderBefore(block, cardTree.contents)) + }, + ".jpg,.jpeg,.png") + + break + } + case "delete": { + mutator.deleteBlock(block) + break + } + } + } + OldMenu.shared.showAtElement(e.target as HTMLElement) + } + + private iconClicked(e: React.MouseEvent) { + const { card } = this.props + + OldMenu.shared.options = [ + { id: "random", name: "Random" }, + { id: "remove", name: "Remove Icon" }, + ] + OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => { + switch (optionId) { + case "remove": + mutator.changeIcon(card, undefined, "remove icon") + break + case "random": + const newIcon = BlockIcons.shared.randomIcon() + mutator.changeIcon(card, newIcon) + break + } + } + OldMenu.shared.showAtElement(e.target as HTMLElement) + } + + close() { + OldMenu.shared.hide() + PropertyMenu.shared.hide() + } +} diff --git a/src/client/components/cardDialog.tsx b/src/client/components/cardDialog.tsx index a56ae4d0b..df3c9ad71 100644 --- a/src/client/components/cardDialog.tsx +++ b/src/client/components/cardDialog.tsx @@ -1,21 +1,10 @@ import React from "react" -import { Block } from "../blocks/block" import { Card } from "../blocks/card" -import { BlockIcons } from "../blockIcons" import { BoardTree } from "../boardTree" -import { CardTree } from "../cardTree" -import { Menu, MenuOption } from "../menu" import mutator from "../mutator" -import { IBlock, IOrderedBlock } from "../octoTypes" -import { OctoUtils } from "../octoUtils" -import { PropertyMenu } from "../propertyMenu" -import { OctoListener } from "../octoListener" -import { Utils } from "../utils" -import Button from "./button" -import { Editable } from "./editable" -import { MarkdownEditor } from "./markdownEditor" -import { TextBlock } from "../blocks/textBlock" -import { CommentBlock } from "../blocks/commentBlock" +import Menu from "../widgets/menu" +import CardDetail from "./cardDetail" +import Dialog from "./dialog" type Props = { boardTree: BoardTree @@ -23,441 +12,21 @@ type Props = { onClose: () => void } -type State = { - isHoverOnCover: boolean - cardTree?: CardTree -} - -class CardDialog extends React.Component { - private titleRef = React.createRef() - private cardListener: OctoListener - - constructor(props: Props) { - super(props) - this.state = { isHoverOnCover: false } - } - - keydownHandler = (e: KeyboardEvent) => { - if (e.target !== document.body) { return } - - if (e.keyCode === 27) { - this.close() - e.stopPropagation() - } - } - - componentDidMount() { - this.cardListener = new OctoListener() - this.cardListener.open(this.props.card.id, async () => { - await cardTree.sync() - this.setState({ cardTree }) - }) - const cardTree = new CardTree(this.props.card.id) - cardTree.sync().then(() => { - this.setState({cardTree}) - }) - - document.addEventListener("keydown", this.keydownHandler) - } - - componentDidUpdate(prevProps: Props, prevState: State) { - if (this.titleRef.current && prevState.cardTree === undefined && this.state.cardTree !== undefined) { - this.titleRef.current.focus() - } - } - - componentWillUnmount() { - document.removeEventListener("keydown", this.keydownHandler) - } - +class CardDialog extends React.Component { render() { - const { boardTree, card } = this.props - const { cardTree } = this.state - const { board } = boardTree - if (cardTree === undefined) { - return null - } - - const { comments } = cardTree - - const newCommentPlaceholderText = "Add a comment..." - - const backgroundRef = React.createRef() - const newCommentRef = React.createRef() - const sendCommentButtonRef = React.createRef() - let contentElements - if (cardTree.contents.length > 0) { - contentElements = -
- {cardTree.contents.map(block => { - if (block.type === "text") { - const cardText = block.title - return
-
-
{ this.showContentBlockMenu(e, block) }}> -
-
-
- { - 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) }}> -
-
-
- {block.title} -
- } - - return
- })} -
- - } else { - contentElements =
-
-
- { - const block = new TextBlock({ parentId: card.id, title: text }) - block.order = cardTree.contents.length * 1000 - mutator.insertBlock(block, "add card text") - }} /> -
-
- } - - const icon = card.icon - - // TODO: Replace this placeholder - const username = "John Smith" - const userImageUrl = `data:image/svg+xml,` - - const element = -
{ - if (e.target === backgroundRef.current) { this.close() } - }}> -
-
-
- -
-
- {icon ? -
{ this.iconClicked(e) }}>{icon}
- : undefined - } -
{ this.setState({ ...this.state, isHoverOnCover: true }) }} - onMouseLeave={() => { this.setState({ ...this.state, isHoverOnCover: false }) }} - > - -
- - { mutator.changeTitle(card, text) }} /> - - {/* Property list */} - -
- {board.cardProperties.map(propertyTemplate => { - return ( -
-
{ - const menu = PropertyMenu.shared - menu.property = propertyTemplate - menu.onNameChanged = (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") - break - case "type-updatedTime": - await mutator.changePropertyType(board, propertyTemplate, "updatedTime") - break - case "type-select": - await mutator.changePropertyType(board, propertyTemplate, "select") - break - case "delete": - await mutator.deleteProperty(boardTree, propertyTemplate.id) - break - 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) - }}>+ Add a property
-
- - {/* Comments */} - -
-
- {comments.map(comment => { - const optionsButtonRef = React.createRef() - const showCommentMenu = (e: React.MouseEvent, activeComment: IBlock) => { - Menu.shared.options = [ - { id: "delete", name: "Delete" } - ] - Menu.shared.onMenuClicked = (id) => { - switch (id) { - case "delete": { - mutator.deleteBlock(activeComment) - break - } - } - } - Menu.shared.showAtElement(e.target as HTMLElement) - } - - return
{ optionsButtonRef.current.style.display = null }} onMouseLeave={() => { optionsButtonRef.current.style.display = "none" }}> -
- -
{username}
-
{(new Date(comment.createAt)).toLocaleTimeString()}
-
{ showCommentMenu(e, comment) }}>...
-
-
{comment.title}
-
- })} - - {/* New comment */} - -
- - { return }} - onFocus={() => { - sendCommentButtonRef.current.style.display = null - }} - 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() - } - }} - > - -
{ - const text = newCommentRef.current.text - console.log(`Send comment: ${newCommentRef.current.text}`) - this.sendComment(text) - newCommentRef.current.text = undefined - newCommentRef.current.blur() - }} - >Send
-
-
- -
-
- - {/* Content blocks */} - -
- {contentElements} -
- -
-
-
{ - Menu.shared.options = [ - { id: "text", name: "Text" }, - { id: "image", name: "Image" }, - ] - Menu.shared.onMenuClicked = async (optionId: string, type?: string) => { - switch (optionId) { - case "text": - const block = new TextBlock({ parentId: card.id }) - block.order = cardTree.contents.length * 1000 - await mutator.insertBlock(block, "add text") - break - case "image": - Utils.selectLocalFile( - (file) => { - mutator.createImageBlock(card.id, file, cardTree.contents.length * 1000) - }, - ".jpg,.jpeg,.png") - break - } - } - Menu.shared.showAtElement(e.target as HTMLElement) - }} - >Add content
-
-
- -
-
- - return element - } - - async sendComment(text: string) { - const { card } = this.props - - Utils.assertValue(card) - - const block = new CommentBlock({ parentId: card.id, title: text }) - await mutator.insertBlock(block, "add comment") - } - - private showContentBlockMenu(e: React.MouseEvent, block: IOrderedBlock) { - const { card } = this.props - const { cardTree } = this.state - const index = cardTree.contents.indexOf(block) - - const options: MenuOption[] = [] - if (index > 0) { - options.push({ id: "moveUp", name: "Move up" }) - } - if (index < cardTree.contents.length - 1) { - options.push({ id: "moveDown", name: "Move down" }) - } - - options.push( - { id: "insertAbove", name: "Insert above", type: "submenu" }, - { id: "delete", name: "Delete" } - ) - - Menu.shared.options = options - Menu.shared.subMenuOptions.set("insertAbove", [ - { id: "text", name: "Text" }, - { id: "image", name: "Image" }, - ]) - Menu.shared.onMenuClicked = (optionId: string, type?: string) => { - 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) - Utils.log(`moveUp ${newOrder}`) - mutator.changeOrder(block, newOrder, "move up") - break - } - 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) - Utils.log(`moveDown ${newOrder}`) - mutator.changeOrder(block, newOrder, "move down") - break - } - case "insertAbove-text": { - const newBlock = new TextBlock({ parentId: card.id }) - newBlock.order = OctoUtils.getOrderBefore(block, cardTree.contents) - // TODO: Handle need to reorder all blocks - Utils.log(`insert block ${newBlock.id}, order: ${newBlock.order}`) - mutator.insertBlock(newBlock, "insert card text") - break - } - case "insertAbove-image": { - Utils.selectLocalFile( - (file) => { - mutator.createImageBlock(card.id, file, OctoUtils.getOrderBefore(block, cardTree.contents)) - }, - ".jpg,.jpeg,.png") - - break - } - case "delete": { - mutator.deleteBlock(block) - break - } - } - } - Menu.shared.showAtElement(e.target as HTMLElement) - } - - private iconClicked(e: React.MouseEvent) { - const { card } = this.props - - Menu.shared.options = [ - { id: "random", name: "Random" }, - { id: "remove", name: "Remove Icon" }, - ] - Menu.shared.onMenuClicked = (optionId: string, type?: string) => { - switch (optionId) { - case "remove": - mutator.changeIcon(card, undefined, "remove icon") - break - case "random": - const newIcon = BlockIcons.shared.randomIcon() - mutator.changeIcon(card, newIcon) - break - } - } - Menu.shared.showAtElement(e.target as HTMLElement) - } - - close() { - Menu.shared.hide() - PropertyMenu.shared.hide() - - this.props.onClose() + const menu = ( + + { + await mutator.deleteBlock(this.props.card, "delete card") + this.props.onClose() + }}/> + + ) + return ( + + + + ) } } diff --git a/src/client/components/dialog.tsx b/src/client/components/dialog.tsx new file mode 100644 index 000000000..139836b24 --- /dev/null +++ b/src/client/components/dialog.tsx @@ -0,0 +1,55 @@ +import React from "react" +import MenuWrapper from "../widgets/menuWrapper" +import Button from "./button" + +type Props = { + children: React.ReactNode + toolsMenu: React.ReactNode + onClose: () => void +} + +export default class Dialog extends React.Component { + keydownHandler = (e: KeyboardEvent) => { + if (e.target !== document.body) { return } + + if (e.keyCode === 27) { + this.close() + e.stopPropagation() + } + } + + componentDidMount() { + document.addEventListener("keydown", this.keydownHandler) + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.keydownHandler) + } + + render() { + const {toolsMenu} = this.props + + return ( +
{ if (e.target === e.currentTarget) { this.close() } }} + > +
+ {toolsMenu && +
+
+ + + {toolsMenu} + +
} + {this.props.children} +
+
+ ) + } + + close() { + this.props.onClose() + } +}