diff --git a/src/client/app.tsx b/src/client/app.tsx index fed73ca09..dc5c79d77 100644 --- a/src/client/app.tsx +++ b/src/client/app.tsx @@ -8,7 +8,7 @@ import { } from "react-router-dom"; import LoginPage from './pages/loginPage'; -import HomePage from './pages/homePage'; +import BoardPage from './pages/boardPage'; export default function App() { return ( @@ -23,8 +23,8 @@ export default function App() { - - + + diff --git a/src/client/boardPage.tsx b/src/client/boardPage.tsx deleted file mode 100644 index bc9009a62..000000000 --- a/src/client/boardPage.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom" -import { BoardTree } from "./boardTree" -import { BoardView } from "./boardView" -import { CardTree } from "./cardTree" -import { CardDialog } from "./components/cardDialog" -import { FilterComponent } from "./components/filterComponent" -import { PageHeader } from "./components/pageHeader" -import { WorkspaceComponent } from "./components/workspaceComponent" -import { FlashMessage } from "./flashMessage" -import { Mutator } from "./mutator" -import { OctoClient } from "./octoClient" -import { OctoListener } from "./octoListener" -import { IBlock, IPageController } from "./octoTypes" -import { UndoManager } from "./undomanager" -import { Utils } from "./utils" -import { WorkspaceTree } from "./workspaceTree" - -class BoardPage implements IPageController { - boardTitle: HTMLElement - mainBoardHeader: HTMLElement - mainBoardBody: HTMLElement - groupByButton: HTMLElement - groupByLabel: HTMLElement - - boardId?: string - viewId?: string - - workspaceTree: WorkspaceTree - boardTree?: BoardTree - view: BoardView - - updateTitleTimeout: number - updatePropertyLabelTimeout: number - - shownCardTree: CardTree - - private filterAnchorElement?: HTMLElement - private octo = new OctoClient() - private boardListener = new OctoListener() - private cardListener = new OctoListener() - - constructor() { - const queryString = new URLSearchParams(window.location.search) - const boardId = queryString.get("id") - const viewId = queryString.get("v") - - this.layoutPage() - - this.workspaceTree = new WorkspaceTree(this.octo) - - console.log(`BoardPage. boardId: ${this.boardId}`) - if (boardId) { - this.attachToBoard(boardId, viewId) - } else { - this.sync() - } - - document.body.addEventListener("keydown", async (e) => { - if (e.target !== document.body) { return } - - if (e.keyCode === 90 && !e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Cmd+Z - Utils.log(`Undo`) - const description = UndoManager.shared.undoDescription - await UndoManager.shared.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 - Utils.log(`Redo`) - const description = UndoManager.shared.redoDescription - await UndoManager.shared.redo() - if (description) { - FlashMessage.show(`Redo ${description}`) - } else { - FlashMessage.show(`Redo`) - } - } - }) - - this.render() - } - - private layoutPage() { - const root = Utils.getElementById("octo-tasks-app") - root.innerText = "" - - const header = root.appendChild(document.createElement("div")) - header.id = "header" - - const main = root.appendChild(document.createElement("div")) - main.id = "main" - - const overlay = root.appendChild(document.createElement("div")) - overlay.id = "overlay" - - const modal = root.appendChild(document.createElement("div")) - modal.id = "modal" - } - - render() { - const { octo, boardTree } = this - const { board, activeView } = boardTree || {} - const mutator = new Mutator(octo) - - const mainElement = Utils.getElementById("main") - - ReactDOM.render( - , - Utils.getElementById("header") - ) - - if (board) { - Utils.setFavicon(board.icon) - document.title = `OCTO - ${board.title} | ${activeView.title}` - } - - ReactDOM.render( - , - mainElement - ) - - if (boardTree && boardTree.board && this.shownCardTree) { - ReactDOM.render( - { this.showCard(undefined) }}>, - Utils.getElementById("overlay") - ) - } else { - ReactDOM.render( - , - Utils.getElementById("overlay") - ) - } - - if (this.filterAnchorElement) { - const element = this.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 pageY = rect.bottom - bodyRect.top - - ReactDOM.render( - { this.showFilter(undefined) }} - > - , - Utils.getElementById("modal") - ) - } else { - ReactDOM.render(, Utils.getElementById("modal")) - } - } - - private attachToBoard(boardId: string, viewId?: string) { - this.boardId = boardId - this.viewId = viewId - - this.boardTree = new BoardTree(this.octo, boardId) - - this.boardListener.open(boardId, (blockId: string) => { - console.log(`octoListener.onChanged: ${blockId}`) - this.sync() - }) - - this.sync() - } - - async sync() { - const { workspaceTree, boardTree } = this - - await workspaceTree.sync() - if (boardTree) { - await boardTree.sync() - - // Default to first view - if (!this.viewId) { - this.viewId = boardTree.views[0].id - } - - boardTree.setActiveView(this.viewId) - // TODO: Handle error (viewId not found) - this.viewId = boardTree.activeView.id - console.log(`sync complete... title: ${boardTree.board.title}`) - } - - this.render() - } - - // IPageController - - async showCard(card: IBlock) { - this.cardListener.close() - - if (card) { - const cardTree = new CardTree(this.octo, card.id) - await cardTree.sync() - this.shownCardTree = cardTree - - this.cardListener = new OctoListener() - this.cardListener.open(card.id, async () => { - await cardTree.sync() - this.render() - }) - } else { - this.shownCardTree = undefined - } - - this.render() - } - - showBoard(boardId: string) { - if (this.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) - - this.attachToBoard(boardId) - } - - showView(viewId: string) { - this.viewId = viewId - this.boardTree.setActiveView(this.viewId) - const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + `?id=${encodeURIComponent(this.boardId)}&v=${encodeURIComponent(viewId)}` - window.history.pushState({ path: newUrl }, "", newUrl) - this.render() - } - - showFilter(ahchorElement?: HTMLElement) { - this.filterAnchorElement = ahchorElement - this.render() - } - - setSearchText(text?: string) { - this.boardTree.setSearchText(text) - this.render() - } -} - -export { BoardPage } - -const _ = new BoardPage() -console.log("BoardPage") diff --git a/src/client/components/boardComponent.tsx b/src/client/components/boardComponent.tsx index 5284003e2..a598de480 100644 --- a/src/client/components/boardComponent.tsx +++ b/src/client/components/boardComponent.tsx @@ -8,7 +8,7 @@ import { CardFilter } from "../cardFilter" import { Constants } from "../constants" import { Menu } from "../menu" import { Mutator } from "../mutator" -import { IBlock, IPageController } from "../octoTypes" +import { IBlock } from "../octoTypes" import { OctoUtils } from "../octoUtils" import { Utils } from "../utils" import { BoardCard } from "./boardCard" @@ -19,7 +19,10 @@ import { Editable } from "./editable" type Props = { mutator: Mutator, boardTree?: BoardTree - pageController: IPageController + showView: (id: string) => void + showCard: (card: IBlock) => void + showFilter: (el: HTMLElement) => void + setSearchText: (text: string) => void } type State = { @@ -44,7 +47,7 @@ class BoardComponent extends React.Component { } render() { - const { mutator, boardTree, pageController } = this.props + const { mutator, boardTree, showView } = this.props if (!boardTree || !boardTree.board) { return ( @@ -88,7 +91,7 @@ class BoardComponent extends React.Component { { mutator.changeTitle(activeView, text) }} /> - { OctoUtils.showViewMenu(e, mutator, boardTree, pageController) }}> + { OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}> { this.propertiesClicked(e) }}>Properties { this.groupByClicked(e) }}> @@ -223,7 +226,7 @@ class BoardComponent extends React.Component { async showCard(card?: IBlock) { console.log(`showCard: ${card?.title}`) - await this.props.pageController.showCard(card) + await this.props.showCard(card) } async addCard(groupByValue?: string) { @@ -270,8 +273,7 @@ class BoardComponent extends React.Component { } private filterClicked(e: React.MouseEvent) { - const { pageController } = this.props - pageController.showFilter(e.target as HTMLElement) + this.props.showFilter(e.target as HTMLElement) } private async optionsClicked(e: React.MouseEvent) { @@ -403,13 +405,13 @@ class BoardComponent extends React.Component { if (e.keyCode === 27) { // ESC: Clear search this.searchFieldRef.current.text = "" this.setState({ ...this.state, isSearching: false }) - this.props.pageController.setSearchText(undefined) + this.props.setSearchText(undefined) e.preventDefault() } } searchChanged(text?: string) { - this.props.pageController.setSearchText(text) + this.props.setSearchText(text) } } diff --git a/src/client/components/sidebar.tsx b/src/client/components/sidebar.tsx index 5ce027fdc..d4b64d647 100644 --- a/src/client/components/sidebar.tsx +++ b/src/client/components/sidebar.tsx @@ -9,7 +9,7 @@ import { WorkspaceTree } from "../workspaceTree" type Props = { mutator: Mutator - pageController: IPageController + showBoard: (id: string) => void workspaceTree: WorkspaceTree, boardTree?: BoardTree } @@ -47,7 +47,7 @@ class Sidebar extends React.Component { } private showOptions(e: React.MouseEvent, board: Board) { - const { mutator, pageController, workspaceTree } = this.props + const { mutator, showBoard, workspaceTree } = this.props const { boards } = workspaceTree const options: MenuOption[] = [] @@ -64,8 +64,8 @@ class Sidebar extends React.Component { mutator.deleteBlock( board, "delete block", - async () => { pageController.showBoard(nextBoardId!) }, - async () => { pageController.showBoard(board.id) }, + async () => { showBoard(nextBoardId!) }, + async () => { showBoard(board.id) }, ) break } @@ -104,20 +104,19 @@ class Sidebar extends React.Component { } private boardClicked(board: Board) { - const { pageController } = this.props - pageController.showBoard(board.id) + this.props.showBoard(board.id) } async addBoardClicked() { - const { mutator, boardTree, pageController } = this.props + const { mutator, boardTree, showBoard } = this.props const oldBoardId = boardTree?.board?.id const board = new Board() await mutator.insertBlock( board, "add board", - async () => { pageController.showBoard(board.id) }, - async () => { if (oldBoardId) { pageController.showBoard(oldBoardId) } }) + async () => { showBoard(board.id) }, + async () => { if (oldBoardId) { showBoard(oldBoardId) } }) await mutator.insertBlock(board) } diff --git a/src/client/components/tableComponent.tsx b/src/client/components/tableComponent.tsx index fc111f38f..016afa318 100644 --- a/src/client/components/tableComponent.tsx +++ b/src/client/components/tableComponent.tsx @@ -7,7 +7,7 @@ import { BoardTree } from "../boardTree" import { CsvExporter } from "../csvExporter" import { Menu } from "../menu" import { Mutator } from "../mutator" -import { IBlock, IPageController } from "../octoTypes" +import { IBlock } from "../octoTypes" import { OctoUtils } from "../octoUtils" import { Utils } from "../utils" import Button from "./button" @@ -17,7 +17,10 @@ import { TableRow } from "./tableRow" type Props = { mutator: Mutator, boardTree?: BoardTree - pageController: IPageController + showView: (id: string) => void + showCard: (card: IBlock) => void + showFilter: (el: HTMLElement) => void + setSearchText: (text: string) => void } type State = { @@ -43,7 +46,7 @@ class TableComponent extends React.Component { } render() { - const { mutator, boardTree, pageController } = this.props + const { mutator, boardTree, showView } = this.props if (!boardTree || !boardTree.board) { return ( @@ -85,7 +88,7 @@ class TableComponent extends React.Component { { mutator.changeTitle(activeView, text) }} /> - { OctoUtils.showViewMenu(e, mutator, boardTree, pageController) }}> + { OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}> { this.propertiesClicked(e) }}>Properties { this.filterClicked(e) }}>Filter @@ -243,8 +246,7 @@ class TableComponent extends React.Component { } private filterClicked(e: React.MouseEvent) { - const { pageController } = this.props - pageController.showFilter(e.target as HTMLElement) + this.props.showFilter(e.target as HTMLElement) } private async optionsClicked(e: React.MouseEvent) { @@ -348,7 +350,7 @@ class TableComponent extends React.Component { async showCard(card: IBlock) { console.log(`showCard: ${card.title}`) - await this.props.pageController.showCard(card) + await this.props.showCard(card) } focusOnCardTitle(cardId: string) { @@ -396,13 +398,13 @@ class TableComponent extends React.Component { if (e.keyCode === 27) { // ESC: Clear search this.searchFieldRef.current.text = "" this.setState({ ...this.state, isSearching: false }) - this.props.pageController.setSearchText(undefined) + this.props.setSearchText(undefined) e.preventDefault() } } searchChanged(text?: string) { - this.props.pageController.setSearchText(text) + this.props.setSearchText(text) } } diff --git a/src/client/components/workspaceComponent.tsx b/src/client/components/workspaceComponent.tsx index 30342b2b4..9d19e426b 100644 --- a/src/client/components/workspaceComponent.tsx +++ b/src/client/components/workspaceComponent.tsx @@ -1,7 +1,7 @@ import React from "react" import { BoardTree } from "../boardTree" import { Mutator } from "../mutator" -import { IPageController } from "../octoTypes" +import { IBlock } from "../octoTypes" import { Utils } from "../utils" import { WorkspaceTree } from "../workspaceTree" import { BoardComponent } from "./boardComponent" @@ -12,16 +12,20 @@ type Props = { mutator: Mutator, workspaceTree: WorkspaceTree boardTree?: BoardTree - pageController: IPageController + showBoard: (id: string) => void + showView: (id: string) => void + showCard: (card: IBlock) => void + showFilter: (el: HTMLElement) => void + setSearchText: (text: string) => void } class WorkspaceComponent extends React.Component { render() { - const { mutator, boardTree, workspaceTree, pageController } = this.props + const { mutator, boardTree, workspaceTree, showBoard} = this.props const element = - + {this.mainComponent()} @@ -29,7 +33,7 @@ class WorkspaceComponent extends React.Component { } private mainComponent() { - const { mutator, boardTree, pageController } = this.props + const { mutator, boardTree, showCard, showFilter, setSearchText, showView } = this.props const { activeView } = boardTree || {} if (!activeView) { @@ -38,11 +42,11 @@ class WorkspaceComponent extends React.Component { switch (activeView?.viewType) { case "board": { - return + return } case "table": { - return + return } default: { diff --git a/src/client/octoUtils.tsx b/src/client/octoUtils.tsx index 0df9702e0..8b96188a5 100644 --- a/src/client/octoUtils.tsx +++ b/src/client/octoUtils.tsx @@ -9,7 +9,7 @@ import { IBlock, IPageController } from "./octoTypes" import { Utils } from "./utils" class OctoUtils { - static async showViewMenu(e: React.MouseEvent, mutator: Mutator, boardTree: BoardTree, pageController: IPageController) { + 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" })) @@ -33,7 +33,7 @@ class OctoUtils { const view = boardTree.activeView const nextView = boardTree.views.find(o => o !== view) await mutator.deleteBlock(view, "delete view") - pageController.showView(nextView.id) + showView(nextView.id) break } case "__addview-board": { @@ -48,8 +48,8 @@ class OctoUtils { await mutator.insertBlock( view, "add view", - async () => { pageController.showView(view.id) }, - async () => { pageController.showView(oldViewId) }) + async () => { showView(view.id) }, + async () => { showView(oldViewId) }) break } case "__addview-table": { @@ -65,13 +65,13 @@ class OctoUtils { await mutator.insertBlock( view, "add view", - async () => { pageController.showView(view.id) }, - async () => { pageController.showView(oldViewId) }) + async () => { showView(view.id) }, + async () => { showView(oldViewId) }) break } default: { const view = boardTree.views.find(o => o.id === optionId) - pageController.showView(view.id) + showView(view.id) } } } diff --git a/src/client/pages/boardPage.tsx b/src/client/pages/boardPage.tsx new file mode 100644 index 000000000..20ece3b9f --- /dev/null +++ b/src/client/pages/boardPage.tsx @@ -0,0 +1,239 @@ +import React from "react" +import ReactDOM from "react-dom" +import { BoardTree } from "../boardTree" +import { BoardView } from "../boardView" +import { CardTree } from "../cardTree" +import { CardDialog } from "../components/cardDialog" +import { FilterComponent } from "../components/filterComponent" +import { PageHeader } from "../components/pageHeader" +import { WorkspaceComponent } from "../components/workspaceComponent" +import { FlashMessage } from "../flashMessage" +import { Mutator } from "../mutator" +import { OctoClient } from "../octoClient" +import { OctoListener } from "../octoListener" +import { IBlock, IPageController } from "../octoTypes" +import { UndoManager } from "../undomanager" +import { Utils } from "../utils" +import { WorkspaceTree } from "../workspaceTree" + +type Props = { +} + +type State = { + boardId: string + viewId: string + workspaceTree: WorkspaceTree + boardTree?: BoardTree +} + +export default class BoardPage extends React.Component { + workspaceTree: WorkspaceTree + boardTree?: BoardTree + view: BoardView + + updateTitleTimeout: number + updatePropertyLabelTimeout: number + + shownCardTree: CardTree + + private filterAnchorElement?: HTMLElement + private octo = new OctoClient() + private boardListener = new OctoListener() + private cardListener = new OctoListener() + + constructor(props: Props) { + super(props) + const queryString = new URLSearchParams(window.location.search) + const boardId = queryString.get("id") + const viewId = queryString.get("v") + + this.state = { + boardId, + viewId, + workspaceTree: new WorkspaceTree(this.octo), + } + + console.log(`BoardPage. boardId: ${boardId}`) + } + + componentDidUpdate(prevProps: Props, prevState: State) { + const board = this.state.boardTree.board; + const prevBoard = prevState.boardTree.board; + + const activeView = this.state.boardTree.activeView; + const prevActiveView = prevState.boardTree.activeView; + + if (board.icon !== prevBoard.icon) { + Utils.setFavicon(board.icon) + } + if (board.title !== prevBoard.title || activeView.title !== prevActiveView.title) { + document.title = `OCTO - ${board.title} | ${activeView.title}` + } + } + + undoRedoHandler = async (e: KeyboardEvent) => { + if (e.target !== document) { return } + + if (e.keyCode === 90 && !e.shiftKey && (e.ctrlKey || e.metaKey) && !e.altKey) { // Cmd+Z + Utils.log(`Undo`) + const description = UndoManager.shared.undoDescription + await UndoManager.shared.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 + Utils.log(`Redo`) + const description = UndoManager.shared.redoDescription + await UndoManager.shared.redo() + if (description) { + FlashMessage.show(`Redo ${description}`) + } else { + FlashMessage.show(`Redo`) + } + } + } + + componentDidMount() { + document.addEventListener("keydown", this.undoRedoHandler) + if (this.state.boardId) { + this.attachToBoard(this.state.boardId, this.state.viewId) + } else { + this.sync() + } + } + + componentWillUnmount() { + document.removeEventListener("keydown", this.undoRedoHandler) + } + + render() { + const { board, activeView } = this.state.boardTree || {} + const mutator = new Mutator(this.octo) + + // TODO Move all this into the root portal component when that is merged + if (this.state.boardTree && this.state.boardTree.board && this.shownCardTree) { + ReactDOM.render( + { this.showCard(undefined) }}>, + Utils.getElementById("overlay") + ) + } else { + ReactDOM.render( + , + Utils.getElementById("overlay") + ) + } + + if (this.filterAnchorElement) { + const element = this.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 pageY = rect.bottom - bodyRect.top + + ReactDOM.render( + { this.showFilter(undefined) }} + > + , + Utils.getElementById("modal") + ) + } else { + ReactDOM.render(, Utils.getElementById("modal")) + } + + return ( + + , + + ); + } + + private attachToBoard(boardId: string, viewId?: string) { + const boardTree = new BoardTree(this.octo, boardId) + this.setState({ + boardId, + viewId, + boardTree, + }) + + this.boardListener.open(boardId, (blockId: string) => { + console.log(`octoListener.onChanged: ${blockId}`) + this.sync() + }) + + this.sync() + } + + async sync() { + const { viewId, workspaceTree, boardTree } = this.state + + await workspaceTree.sync() + if (boardTree) { + await boardTree.sync() + + // Default to first view + if (!viewId) { + this.setState({viewId: boardTree.views[0].id}) + } + + boardTree.setActiveView(this.state.viewId) + // TODO: Handle error (viewId not found) + this.setState({ + viewId: boardTree.activeView.id + }) + console.log(`sync complete... title: ${boardTree.board.title}`) + } + } + + // IPageController + + async showCard(card: IBlock) { + this.cardListener.close() + + if (card) { + const cardTree = new CardTree(this.octo, card.id) + await cardTree.sync() + this.shownCardTree = cardTree + + this.cardListener = new OctoListener() + this.cardListener.open(card.id, async () => { + await cardTree.sync() + this.render() + }) + } else { + this.shownCardTree = undefined + } + } + + showBoard(boardId: string) { + if (this.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) + + this.attachToBoard(boardId) + } + + showView(viewId: string) { + this.state.boardTree.setActiveView(viewId) + this.setState({viewId, boardTree: this.state.boardTree}) + const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + `?id=${encodeURIComponent(this.state.boardId)}&v=${encodeURIComponent(viewId)}` + window.history.pushState({ path: newUrl }, "", newUrl) + } + + showFilter(ahchorElement?: HTMLElement) { + this.filterAnchorElement = ahchorElement + } + + setSearchText(text?: string) { + this.boardTree.setSearchText(text) + } +} diff --git a/webpack.common.js b/webpack.common.js index 500b9c4ba..3e6ff6366 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -51,7 +51,7 @@ function makeCommonConfig() { inject: true, title: "OCTO", chunks: ["main"], - template: "html-template/page.ejs", + template: "html-templates/page.ejs", filename: 'index.html' }), ],