diff --git a/src/client/components/boardComponent.tsx b/src/client/components/boardComponent.tsx index 3e59cc252..510c37d54 100644 --- a/src/client/components/boardComponent.tsx +++ b/src/client/components/boardComponent.tsx @@ -5,8 +5,9 @@ import { BlockIcons } from "../blockIcons" import { IPropertyOption } from "../board" import { BoardTree } from "../boardTree" import { CardFilter } from "../cardFilter" +import ViewMenu from "../components/viewMenu" import { Constants } from "../constants" -import { Menu } from "../menu" +import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" import { IBlock } from "../octoTypes" import { OctoUtils } from "../octoUtils" @@ -28,6 +29,7 @@ type Props = { type State = { isHoverOnCover: boolean isSearching: boolean + viewMenu: boolean } class BoardComponent extends React.Component { @@ -37,7 +39,7 @@ class BoardComponent extends React.Component { constructor(props: Props) { super(props) - this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText() } + this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText(), viewMenu: false } } componentDidUpdate(prevPros: Props, prevState: State) { @@ -91,7 +93,21 @@ class BoardComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> -
{ OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}>
+
this.setState({ viewMenu: true })} + > + {this.state.viewMenu && + this.setState({ viewMenu: false })} + mutator={mutator} + boardTree={boardTree} + showView={showView} + />} +
+
{ this.propertiesClicked(e) }}>Properties
{ this.groupByClicked(e) }}> @@ -205,11 +221,11 @@ class BoardComponent extends React.Component { const { mutator, boardTree } = this.props const { board } = boardTree - Menu.shared.options = [ + OldMenu.shared.options = [ { id: "random", name: "Random" }, { id: "remove", name: "Remove Icon" }, ] - Menu.shared.onMenuClicked = (optionId: string, type?: string) => { + OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => { switch (optionId) { case "remove": mutator.changeIcon(board, undefined, "remove icon") @@ -220,7 +236,7 @@ class BoardComponent extends React.Component { break } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } async showCard(card?: IBlock) { @@ -250,12 +266,12 @@ class BoardComponent extends React.Component { async valueOptionClicked(e: React.MouseEvent, option: IPropertyOption) { const { mutator, boardTree } = this.props - Menu.shared.options = [ + OldMenu.shared.options = [ { id: "delete", name: "Delete" }, { id: "", name: "", type: "separator" }, ...Constants.menuColors ] - Menu.shared.onMenuClicked = async (optionId: string, type?: string) => { + OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => { switch (optionId) { case "delete": console.log(`Delete property value: ${option.value}`) @@ -269,7 +285,7 @@ class BoardComponent extends React.Component { } } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private filterClicked(e: React.MouseEvent) { @@ -279,13 +295,13 @@ class BoardComponent extends React.Component { private async optionsClicked(e: React.MouseEvent) { const { boardTree } = this.props - Menu.shared.options = [ + OldMenu.shared.options = [ { id: "exportBoardArchive", name: "Export board archive" }, { id: "testAdd100Cards", name: "TEST: Add 100 cards" }, { id: "testAdd1000Cards", name: "TEST: Add 1,000 cards" }, ] - Menu.shared.onMenuClicked = async (id: string) => { + OldMenu.shared.onMenuClicked = async (id: string) => { switch (id) { case "exportBoardArchive": { Archiver.exportBoardTree(boardTree) @@ -299,7 +315,7 @@ class BoardComponent extends React.Component { } } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private async testAddCards(count: number) { @@ -327,12 +343,12 @@ class BoardComponent extends React.Component { const { activeView } = boardTree const selectProperties = boardTree.board.cardProperties - Menu.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 } }) - Menu.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}`) @@ -345,20 +361,20 @@ class BoardComponent extends React.Component { } await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private async groupByClicked(e: React.MouseEvent) { const { mutator, boardTree } = this.props const selectProperties = boardTree.board.cardProperties.filter(o => o.type === "select") - Menu.shared.options = selectProperties.map((o) => { return { id: o.id, name: o.name } }) - Menu.shared.onMenuClicked = async (command: string) => { + OldMenu.shared.options = selectProperties.map((o) => { return { id: o.id, name: o.name } }) + OldMenu.shared.onMenuClicked = async (command: string) => { if (boardTree.activeView.groupById === command) { return } await mutator.changeViewGroupById(boardTree.activeView, command) } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } async addGroupClicked() { diff --git a/src/client/components/rootPortal.tsx b/src/client/components/rootPortal.tsx index aa5ddce90..f646523aa 100644 --- a/src/client/components/rootPortal.tsx +++ b/src/client/components/rootPortal.tsx @@ -1,44 +1,44 @@ // Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React from 'react'; -import ReactDOM from 'react-dom'; -import PropTypes from 'prop-types'; +import React from 'react' +import ReactDOM from 'react-dom' +import PropTypes from 'prop-types' type Props = { - children: React.ReactNode + children: React.ReactNode } export default class RootPortal extends React.PureComponent { - el: HTMLDivElement + el: HTMLDivElement - static propTypes = { - children: PropTypes.node, - } + static propTypes = { + children: PropTypes.node, + } - constructor(props: Props) { - super(props); - this.el = document.createElement('div'); - } + constructor(props: Props) { + super(props) + this.el = document.createElement('div') + } - componentDidMount() { - const rootPortal = document.getElementById('root-portal'); - if (rootPortal) { - rootPortal.appendChild(this.el); - } - } + componentDidMount() { + const rootPortal = document.getElementById('root-portal') + if (rootPortal) { + rootPortal.appendChild(this.el) + } + } - componentWillUnmount() { - const rootPortal = document.getElementById('root-portal'); - if (rootPortal) { - rootPortal.removeChild(this.el); - } - } + componentWillUnmount() { + const rootPortal = document.getElementById('root-portal') + if (rootPortal) { + rootPortal.removeChild(this.el) + } + } - render() { - return ReactDOM.createPortal( - this.props.children, - this.el, - ); - } + render() { + return ReactDOM.createPortal( + this.props.children, + this.el, + ) + } } diff --git a/src/client/components/tableComponent.tsx b/src/client/components/tableComponent.tsx index 988fd133b..a55508ae5 100644 --- a/src/client/components/tableComponent.tsx +++ b/src/client/components/tableComponent.tsx @@ -5,7 +5,8 @@ import { BlockIcons } from "../blockIcons" import { IPropertyTemplate } from "../board" import { BoardTree } from "../boardTree" import { CsvExporter } from "../csvExporter" -import { Menu } from "../menu" +import ViewMenu from "../components/viewMenu" +import { Menu as OldMenu } from "../menu" import { Mutator } from "../mutator" import { IBlock } from "../octoTypes" import { OctoUtils } from "../octoUtils" @@ -26,6 +27,7 @@ type Props = { type State = { isHoverOnCover: boolean isSearching: boolean + viewMenu: boolean } class TableComponent extends React.Component { @@ -36,7 +38,7 @@ class TableComponent extends React.Component { constructor(props: Props) { super(props) - this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText() } + this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText(), viewMenu: false } } componentDidUpdate(prevPros: Props, prevState: State) { @@ -88,7 +90,21 @@ class TableComponent extends React.Component {
{ mutator.changeTitle(activeView, text) }} /> -
{ OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}>
+
this.setState({ viewMenu: true })} + > + {this.state.viewMenu && + this.setState({ viewMenu: false })} + mutator={mutator} + boardTree={boardTree} + showView={showView} + />} +
+
{ this.propertiesClicked(e) }}>Properties
{ this.filterClicked(e) }}>Filter
@@ -201,11 +217,11 @@ class TableComponent extends React.Component { const { mutator, boardTree } = this.props const { board } = boardTree - Menu.shared.options = [ + OldMenu.shared.options = [ { id: "random", name: "Random" }, { id: "remove", name: "Remove Icon" }, ] - Menu.shared.onMenuClicked = (optionId: string, type?: string) => { + OldMenu.shared.onMenuClicked = (optionId: string, type?: string) => { switch (optionId) { case "remove": mutator.changeIcon(board, undefined, "remove icon") @@ -216,7 +232,7 @@ class TableComponent extends React.Component { break } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private async propertiesClicked(e: React.MouseEvent) { @@ -224,12 +240,12 @@ class TableComponent extends React.Component { const { activeView } = boardTree const selectProperties = boardTree.board.cardProperties - Menu.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 } }) - Menu.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}`) @@ -242,7 +258,7 @@ class TableComponent extends React.Component { } await mutator.changeViewVisibleProperties(activeView, newVisiblePropertyIds) } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private filterClicked(e: React.MouseEvent) { @@ -252,12 +268,12 @@ class TableComponent extends React.Component { private async optionsClicked(e: React.MouseEvent) { const { boardTree } = this.props - Menu.shared.options = [ + OldMenu.shared.options = [ { id: "exportCsv", name: "Export to CSV" }, { id: "exportBoardArchive", name: "Export board archive" }, ] - Menu.shared.onMenuClicked = async (id: string) => { + OldMenu.shared.onMenuClicked = async (id: string) => { switch (id) { case "exportCsv": { CsvExporter.exportTableCsv(boardTree) @@ -269,7 +285,7 @@ class TableComponent extends React.Component { } } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } private async headerClicked(e: React.MouseEvent, templateId: string) { @@ -290,8 +306,8 @@ class TableComponent extends React.Component { options.push({ id: "delete", name: "Delete" }) } - Menu.shared.options = options - Menu.shared.onMenuClicked = async (optionId: string, type?: string) => { + OldMenu.shared.options = options + OldMenu.shared.onMenuClicked = async (optionId: string, type?: string) => { switch (optionId) { case "sortAscending": { const newSortOptions = [ @@ -344,7 +360,7 @@ class TableComponent extends React.Component { } } } - Menu.shared.showAtElement(e.target as HTMLElement) + OldMenu.shared.showAtElement(e.target as HTMLElement) } async showCard(card: IBlock) { diff --git a/src/client/components/viewMenu.tsx b/src/client/components/viewMenu.tsx new file mode 100644 index 000000000..e5b48c34f --- /dev/null +++ b/src/client/components/viewMenu.tsx @@ -0,0 +1,84 @@ +import React from "react" +import { Board } from "../board" +import { BoardTree } from "../boardTree" +import { BoardView } from "../boardView" +import { Mutator } from "../mutator" +import { Utils } from "../utils" +import Menu from "../widgets/menu" + +type Props = { + mutator: Mutator, + boardTree?: BoardTree + board: Board, + showView: (id: string) => void + onClose: () => void, +} + +export default class ViewMenu extends React.Component { + handleDeleteView = async (id: string) => { + const { board, boardTree, mutator, showView } = this.props + Utils.log(`deleteView`) + const view = boardTree.activeView + const nextView = boardTree.views.find(o => o !== view) + await mutator.deleteBlock(view, "delete view") + 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) + } + + handleAddViewBoard = async (id: string) => { + const { board, boardTree, mutator, showView } = this.props + Utils.log(`addview-board`) + const view = new BoardView() + view.title = "Board View" + view.viewType = "board" + view.parentId = board.id + + const oldViewId = boardTree.activeView.id + + await mutator.insertBlock( + view, + "add view", + async () => { showView(view.id) }, + async () => { showView(oldViewId) }) + } + + handleAddViewTable = async (id: string) => { + const { board, boardTree, mutator, showView } = this.props + + Utils.log(`addview-table`) + const view = new BoardView() + view.title = "Table View" + view.viewType = "table" + view.parentId = board.id + view.visiblePropertyIds = board.cardProperties.map(o => o.id) + + const oldViewId = boardTree.activeView.id + + await mutator.insertBlock( + view, + "add view", + async () => { showView(view.id) }, + async () => { showView(oldViewId) }) + } + + render() { + const { onClose, boardTree } = this.props + return ( + + {boardTree.views.map((view) => ())} + + {boardTree.views.length > 1 && } + + + + + + ) + } +} diff --git a/src/client/octoUtils.tsx b/src/client/octoUtils.tsx index 474065de0..4fedbd32b 100644 --- a/src/client/octoUtils.tsx +++ b/src/client/octoUtils.tsx @@ -9,75 +9,6 @@ import { IBlock } from "./octoTypes" import { Utils } from "./utils" class OctoUtils { - static async showViewMenu(e: React.MouseEvent, mutator: Mutator, boardTree: BoardTree, showView: (id: string) => void) { - const { board } = boardTree - - const options: MenuOption[] = boardTree.views.map(view => ({ id: view.id, name: view.title || "Untitled View" })) - options.push({ id: "", name: "", type: "separator" }) - if (boardTree.views.length > 1) { - options.push({ id: "__deleteView", name: "Delete View" }) - } - options.push({ id: "__addview", name: "Add View", type: "submenu" }) - - const addViewMenuOptions = [ - { id: "board", name: "Board" }, - { id: "table", name: "Table" } - ] - Menu.shared.subMenuOptions.set("__addview", addViewMenuOptions) - - Menu.shared.options = options - Menu.shared.onMenuClicked = async (optionId: string, type?: string) => { - switch (optionId) { - case "__deleteView": { - Utils.log(`deleteView`) - const view = boardTree.activeView - const nextView = boardTree.views.find(o => o !== view) - await mutator.deleteBlock(view, "delete view") - showView(nextView.id) - break - } - case "__addview-board": { - Utils.log(`addview-board`) - const view = new BoardView() - view.title = "Board View" - view.viewType = "board" - view.parentId = board.id - - const oldViewId = boardTree.activeView.id - - await mutator.insertBlock( - view, - "add view", - async () => { showView(view.id) }, - async () => { showView(oldViewId) }) - break - } - case "__addview-table": { - Utils.log(`addview-table`) - const view = new BoardView() - view.title = "Table View" - view.viewType = "table" - view.parentId = board.id - view.visiblePropertyIds = board.cardProperties.map(o => o.id) - - const oldViewId = boardTree.activeView.id - - await mutator.insertBlock( - view, - "add view", - async () => { showView(view.id) }, - async () => { showView(oldViewId) }) - break - } - default: { - const view = boardTree.views.find(o => o.id === optionId) - showView(view.id) - } - } - } - Menu.shared.showAtElement(e.target as HTMLElement) - } - static propertyDisplayValue(block: IBlock, propertyValue: string | undefined, propertyTemplate: IPropertyTemplate) { let displayValue: string switch (propertyTemplate.type) { diff --git a/src/client/widgets/menu.tsx b/src/client/widgets/menu.tsx new file mode 100644 index 000000000..78de42a8c --- /dev/null +++ b/src/client/widgets/menu.tsx @@ -0,0 +1,159 @@ +import React from 'react'; + +type MenuOptionProps = { + id: string, + name: string, + onClick?: (id: string) => void, +} + +function SeparatorOption() { + return (
) +} + +type SubMenuOptionProps = MenuOptionProps & { +} + +type SubMenuState = { + isOpen: boolean; +} + +class SubMenuOption extends React.Component { + state = { + isOpen: false + } + + handleMouseEnter = () => { + this.setState({isOpen: true}); + } + + close = () => { + this.setState({isOpen: false}); + } + + render() { + return ( +
+
{this.props.name}
+
+ {this.state.isOpen && + + {this.props.children} + + } +
+ ) + } +} + +type ColorOptionProps = MenuOptionProps & { + icon?: "checked" | "sortUp" | "sortDown" | undefined, +} + +class ColorOption extends React.Component { + render() { + const {name, icon} = this.props; + return ( +
+
{name}
+ {icon &&
} +
+
+ ) + } +} + +type SwitchOptionProps = MenuOptionProps & { + isOn: boolean, + icon?: "checked" | "sortUp" | "sortDown" | undefined, +} + +class SwitchOption extends React.Component { + handleOnClick = () => { + this.props.onClick(this.props.id) + } + render() { + const {name, icon, isOn} = this.props; + return ( +
+
{name}
+ {icon &&
} +
+
+
+
+ ); + } +} + +type TextOptionProps = MenuOptionProps & { + icon?: "checked" | "sortUp" | "sortDown" | undefined, +} +class TextOption extends React.Component { + handleOnClick = () => { + this.props.onClick(this.props.id) + } + + render() { + const {name, icon} = this.props; + return ( +
+
{name}
+ {icon &&
} +
+ ); + } +} + +type MenuProps = { + children: React.ReactNode + onClose: () => void +} + +export default class Menu extends React.Component { + static Color = ColorOption + static SubMenu = SubMenuOption + static Switch = SwitchOption + static Separator = SeparatorOption + static Text = TextOption + + onBodyClick = (e: MouseEvent) => { + this.props.onClose() + } + + onBodyKeyDown = (e: KeyboardEvent) => { + // Ignore keydown events on other elements + if (e.target !== document.body) { return } + if (e.keyCode === 27) { + // ESC + this.props.onClose() + e.stopPropagation() + } + } + + componentDidMount() { + document.addEventListener("click", this.onBodyClick) + document.addEventListener("keydown", this.onBodyKeyDown) + } + + componentWillUnmount() { + document.removeEventListener("click", this.onBodyClick) + document.removeEventListener("keydown", this.onBodyKeyDown) + } + + render() { + return ( +
+
+ {this.props.children} +
+
+ ) + } +}