Merge pull request #5 from mattermost/structure-to-use-react-router

Working on some structure to use react-router
This commit is contained in:
chenilim 2020-10-14 11:17:27 -07:00 committed by GitHub
commit 3c76e099c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 566 additions and 301 deletions

109
package-lock.json generated
View file

@ -4,6 +4,14 @@
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@babel/runtime": {
"version": "7.11.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.11.2.tgz",
"integrity": "sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz",
@ -2350,6 +2358,19 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true "dev": true
}, },
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -2361,6 +2382,14 @@
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"homedir-polyfill": { "homedir-polyfill": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@ -2975,6 +3004,15 @@
} }
} }
}, },
"mini-create-react-context": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz",
"integrity": "sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA==",
"requires": {
"@babel/runtime": "^7.5.5",
"tiny-warning": "^1.0.3"
}
},
"minimalistic-assert": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -3469,6 +3507,21 @@
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
"dev": true "dev": true
}, },
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
}
}
},
"pbkdf2": { "pbkdf2": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
@ -3697,6 +3750,37 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"react-router": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-router-dom": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.2.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-simplemde-editor": { "react-simplemde-editor": {
"version": "4.1.3", "version": "4.1.3",
"resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-4.1.3.tgz", "resolved": "https://registry.npmjs.org/react-simplemde-editor/-/react-simplemde-editor-4.1.3.tgz",
@ -3739,6 +3823,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
},
"regex-not": { "regex-not": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@ -3831,6 +3920,11 @@
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=",
"dev": true "dev": true
}, },
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": { "resolve-url": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -4407,6 +4501,16 @@
"setimmediate": "^1.0.4" "setimmediate": "^1.0.4"
} }
}, },
"tiny-invariant": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
"integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"to-arraybuffer": { "to-arraybuffer": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
@ -4701,6 +4805,11 @@
"integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==",
"dev": true "dev": true
}, },
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vm-browserify": { "vm-browserify": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",

View file

@ -12,6 +12,7 @@
"marked": "^1.1.1", "marked": "^1.1.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-simplemde-editor": "^4.1.3" "react-simplemde-editor": "^4.1.3"
}, },
"devDependencies": { "devDependencies": {

View file

@ -347,7 +347,9 @@ func main() {
// Static files // Static files
handleDefault(r, "/") handleDefault(r, "/")
handleStaticFile(r, "/board", "board.html", "text/html; charset=utf-8") handleStaticFile(r, "/login", "index.html", "text/html; charset=utf-8")
handleStaticFile(r, "/board", "index.html", "text/html; charset=utf-8")
handleStaticFile(r, "/main.js", "main.js", "text/javascript; charset=utf-8")
handleStaticFile(r, "/boardPage.js", "boardPage.js", "text/javascript; charset=utf-8") handleStaticFile(r, "/boardPage.js", "boardPage.js", "text/javascript; charset=utf-8")
handleStaticFile(r, "/favicon.ico", "static/favicon.svg", "image/svg+xml; charset=utf-8") handleStaticFile(r, "/favicon.ico", "static/favicon.svg", "image/svg+xml; charset=utf-8")

40
src/client/app.tsx Normal file
View file

@ -0,0 +1,40 @@
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Link
} from "react-router-dom";
import LoginPage from './pages/loginPage';
import BoardPage from './pages/boardPage';
export default function App() {
return (
<Router>
<div>
<header id="header">
<a href="/">OCTO</a>
</header>
<main id="main">
<Switch>
<Route path="/login">
<LoginPage />
</Route>
<Route path="/board">
<BoardPage />
</Route>
</Switch>
</main>
<footer id="footer">
</footer>
<div id="overlay">
</div>
</div>
</Router>
);
}

View file

@ -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(
<PageHeader />,
Utils.getElementById("header")
)
if (board) {
Utils.setFavicon(board.icon)
document.title = `OCTO - ${board.title} | ${activeView.title}`
}
ReactDOM.render(
<WorkspaceComponent mutator={mutator} workspaceTree={this.workspaceTree} boardTree={this.boardTree} pageController={this} />,
mainElement
)
if (boardTree && boardTree.board && this.shownCardTree) {
ReactDOM.render(
<CardDialog mutator={mutator} boardTree={boardTree} cardTree={this.shownCardTree} onClose={() => { this.showCard(undefined) }}></CardDialog>,
Utils.getElementById("overlay")
)
} else {
ReactDOM.render(
<div />,
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(
<FilterComponent
mutator={mutator}
boardTree={boardTree}
pageX={pageX}
pageY={pageY}
onClose={() => { this.showFilter(undefined) }}
>
</FilterComponent>,
Utils.getElementById("modal")
)
} else {
ReactDOM.render(<div />, 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")

View file

@ -8,18 +8,21 @@ import { CardFilter } from "../cardFilter"
import { Constants } from "../constants" import { Constants } from "../constants"
import { Menu } from "../menu" import { Menu } from "../menu"
import { Mutator } from "../mutator" import { Mutator } from "../mutator"
import { IBlock, IPageController } from "../octoTypes" import { IBlock } from "../octoTypes"
import { OctoUtils } from "../octoUtils" import { OctoUtils } from "../octoUtils"
import { Utils } from "../utils" import { Utils } from "../utils"
import { BoardCard } from "./boardCard" import { BoardCard } from "./boardCard"
import { BoardColumn } from "./boardColumn" import { BoardColumn } from "./boardColumn"
import { Button } from "./button" import Button from "./button"
import { Editable } from "./editable" import { Editable } from "./editable"
type Props = { type Props = {
mutator: Mutator, mutator: Mutator,
boardTree?: BoardTree boardTree?: BoardTree
pageController: IPageController showView: (id: string) => void
showCard: (card: IBlock) => void
showFilter: (el: HTMLElement) => void
setSearchText: (text: string) => void
} }
type State = { type State = {
@ -44,7 +47,7 @@ class BoardComponent extends React.Component<Props, State> {
} }
render() { render() {
const { mutator, boardTree, pageController } = this.props const { mutator, boardTree, showView } = this.props
if (!boardTree || !boardTree.board) { if (!boardTree || !boardTree.board) {
return ( return (
@ -88,7 +91,7 @@ class BoardComponent extends React.Component<Props, State> {
<div className="octo-board"> <div className="octo-board">
<div className="octo-controls"> <div className="octo-controls">
<Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} /> <Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} />
<div className="octo-button" style={{ color: "#000000", fontWeight: 600 }} onClick={(e) => { OctoUtils.showViewMenu(e, mutator, boardTree, pageController) }}><div className="imageDropdown"></div></div> <div className="octo-button" style={{ color: "#000000", fontWeight: 600 }} onClick={(e) => { OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}><div className="imageDropdown"></div></div>
<div className="octo-spacer"></div> <div className="octo-spacer"></div>
<div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div> <div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div>
<div className="octo-button" id="groupByButton" onClick={(e) => { this.groupByClicked(e) }}> <div className="octo-button" id="groupByButton" onClick={(e) => { this.groupByClicked(e) }}>
@ -223,7 +226,7 @@ class BoardComponent extends React.Component<Props, State> {
async showCard(card?: IBlock) { async showCard(card?: IBlock) {
console.log(`showCard: ${card?.title}`) console.log(`showCard: ${card?.title}`)
await this.props.pageController.showCard(card) await this.props.showCard(card)
} }
async addCard(groupByValue?: string) { async addCard(groupByValue?: string) {
@ -270,8 +273,7 @@ class BoardComponent extends React.Component<Props, State> {
} }
private filterClicked(e: React.MouseEvent) { private filterClicked(e: React.MouseEvent) {
const { pageController } = this.props this.props.showFilter(e.target as HTMLElement)
pageController.showFilter(e.target as HTMLElement)
} }
private async optionsClicked(e: React.MouseEvent) { private async optionsClicked(e: React.MouseEvent) {
@ -403,13 +405,13 @@ class BoardComponent extends React.Component<Props, State> {
if (e.keyCode === 27) { // ESC: Clear search if (e.keyCode === 27) { // ESC: Clear search
this.searchFieldRef.current.text = "" this.searchFieldRef.current.text = ""
this.setState({ ...this.state, isSearching: false }) this.setState({ ...this.state, isSearching: false })
this.props.pageController.setSearchText(undefined) this.props.setSearchText(undefined)
e.preventDefault() e.preventDefault()
} }
} }
searchChanged(text?: string) { searchChanged(text?: string) {
this.props.pageController.setSearchText(text) this.props.setSearchText(text)
} }
} }

View file

@ -8,7 +8,7 @@ type Props = {
title?: string title?: string
} }
class Button extends React.Component<Props> { export default class Button extends React.Component<Props> {
render() { render() {
const style = {...this.props.style, backgroundColor: this.props.backgroundColor} const style = {...this.props.style, backgroundColor: this.props.backgroundColor}
return ( return (
@ -22,5 +22,3 @@ class Button extends React.Component<Props> {
</div>) </div>)
} }
} }
export { Button }

View file

@ -9,7 +9,7 @@ import { IBlock } from "../octoTypes"
import { OctoUtils } from "../octoUtils" import { OctoUtils } from "../octoUtils"
import { PropertyMenu } from "../propertyMenu" import { PropertyMenu } from "../propertyMenu"
import { Utils } from "../utils" import { Utils } from "../utils"
import { Button } from "./button" import Button from "./button"
import { Editable } from "./editable" import { Editable } from "./editable"
import { MarkdownEditor } from "./markdownEditor" import { MarkdownEditor } from "./markdownEditor"

View file

@ -9,7 +9,7 @@ import { WorkspaceTree } from "../workspaceTree"
type Props = { type Props = {
mutator: Mutator mutator: Mutator
pageController: IPageController showBoard: (id: string) => void
workspaceTree: WorkspaceTree, workspaceTree: WorkspaceTree,
boardTree?: BoardTree boardTree?: BoardTree
} }
@ -47,7 +47,7 @@ class Sidebar extends React.Component<Props> {
} }
private showOptions(e: React.MouseEvent, board: Board) { private showOptions(e: React.MouseEvent, board: Board) {
const { mutator, pageController, workspaceTree } = this.props const { mutator, showBoard, workspaceTree } = this.props
const { boards } = workspaceTree const { boards } = workspaceTree
const options: MenuOption[] = [] const options: MenuOption[] = []
@ -64,8 +64,8 @@ class Sidebar extends React.Component<Props> {
mutator.deleteBlock( mutator.deleteBlock(
board, board,
"delete block", "delete block",
async () => { pageController.showBoard(nextBoardId!) }, async () => { showBoard(nextBoardId!) },
async () => { pageController.showBoard(board.id) }, async () => { showBoard(board.id) },
) )
break break
} }
@ -104,20 +104,19 @@ class Sidebar extends React.Component<Props> {
} }
private boardClicked(board: Board) { private boardClicked(board: Board) {
const { pageController } = this.props this.props.showBoard(board.id)
pageController.showBoard(board.id)
} }
async addBoardClicked() { async addBoardClicked() {
const { mutator, boardTree, pageController } = this.props const { mutator, boardTree, showBoard } = this.props
const oldBoardId = boardTree?.board?.id const oldBoardId = boardTree?.board?.id
const board = new Board() const board = new Board()
await mutator.insertBlock( await mutator.insertBlock(
board, board,
"add board", "add board",
async () => { pageController.showBoard(board.id) }, async () => { showBoard(board.id) },
async () => { if (oldBoardId) { pageController.showBoard(oldBoardId) } }) async () => { if (oldBoardId) { showBoard(oldBoardId) } })
await mutator.insertBlock(board) await mutator.insertBlock(board)
} }

View file

@ -7,17 +7,20 @@ import { BoardTree } from "../boardTree"
import { CsvExporter } from "../csvExporter" import { CsvExporter } from "../csvExporter"
import { Menu } from "../menu" import { Menu } from "../menu"
import { Mutator } from "../mutator" import { Mutator } from "../mutator"
import { IBlock, IPageController } from "../octoTypes" import { IBlock } from "../octoTypes"
import { OctoUtils } from "../octoUtils" import { OctoUtils } from "../octoUtils"
import { Utils } from "../utils" import { Utils } from "../utils"
import { Button } from "./button" import Button from "./button"
import { Editable } from "./editable" import { Editable } from "./editable"
import { TableRow } from "./tableRow" import { TableRow } from "./tableRow"
type Props = { type Props = {
mutator: Mutator, mutator: Mutator,
boardTree?: BoardTree boardTree?: BoardTree
pageController: IPageController showView: (id: string) => void
showCard: (card: IBlock) => void
showFilter: (el: HTMLElement) => void
setSearchText: (text: string) => void
} }
type State = { type State = {
@ -43,7 +46,7 @@ class TableComponent extends React.Component<Props, State> {
} }
render() { render() {
const { mutator, boardTree, pageController } = this.props const { mutator, boardTree, showView } = this.props
if (!boardTree || !boardTree.board) { if (!boardTree || !boardTree.board) {
return ( return (
@ -85,7 +88,7 @@ class TableComponent extends React.Component<Props, State> {
<div className="octo-table"> <div className="octo-table">
<div className="octo-controls"> <div className="octo-controls">
<Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} /> <Editable style={{ color: "#000000", fontWeight: 600 }} text={activeView.title} placeholderText="Untitled View" onChanged={(text) => { mutator.changeTitle(activeView, text) }} />
<div className="octo-button" style={{ color: "#000000", fontWeight: 600 }} onClick={(e) => { OctoUtils.showViewMenu(e, mutator, boardTree, pageController) }}><div className="imageDropdown"></div></div> <div className="octo-button" style={{ color: "#000000", fontWeight: 600 }} onClick={(e) => { OctoUtils.showViewMenu(e, mutator, boardTree, showView) }}><div className="imageDropdown"></div></div>
<div className="octo-spacer"></div> <div className="octo-spacer"></div>
<div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div> <div className="octo-button" onClick={(e) => { this.propertiesClicked(e) }}>Properties</div>
<div className={ hasFilter ? "octo-button active" : "octo-button"} onClick={(e) => { this.filterClicked(e) }}>Filter</div> <div className={ hasFilter ? "octo-button active" : "octo-button"} onClick={(e) => { this.filterClicked(e) }}>Filter</div>
@ -243,8 +246,7 @@ class TableComponent extends React.Component<Props, State> {
} }
private filterClicked(e: React.MouseEvent) { private filterClicked(e: React.MouseEvent) {
const { pageController } = this.props this.props.showFilter(e.target as HTMLElement)
pageController.showFilter(e.target as HTMLElement)
} }
private async optionsClicked(e: React.MouseEvent) { private async optionsClicked(e: React.MouseEvent) {
@ -348,7 +350,7 @@ class TableComponent extends React.Component<Props, State> {
async showCard(card: IBlock) { async showCard(card: IBlock) {
console.log(`showCard: ${card.title}`) console.log(`showCard: ${card.title}`)
await this.props.pageController.showCard(card) await this.props.showCard(card)
} }
focusOnCardTitle(cardId: string) { focusOnCardTitle(cardId: string) {
@ -396,13 +398,13 @@ class TableComponent extends React.Component<Props, State> {
if (e.keyCode === 27) { // ESC: Clear search if (e.keyCode === 27) { // ESC: Clear search
this.searchFieldRef.current.text = "" this.searchFieldRef.current.text = ""
this.setState({ ...this.state, isSearching: false }) this.setState({ ...this.state, isSearching: false })
this.props.pageController.setSearchText(undefined) this.props.setSearchText(undefined)
e.preventDefault() e.preventDefault()
} }
} }
searchChanged(text?: string) { searchChanged(text?: string) {
this.props.pageController.setSearchText(text) this.props.setSearchText(text)
} }
} }

View file

@ -1,7 +1,7 @@
import React from "react" import React from "react"
import { BoardTree } from "../boardTree" import { BoardTree } from "../boardTree"
import { Mutator } from "../mutator" import { Mutator } from "../mutator"
import { IPageController } from "../octoTypes" import { IBlock } from "../octoTypes"
import { Utils } from "../utils" import { Utils } from "../utils"
import { WorkspaceTree } from "../workspaceTree" import { WorkspaceTree } from "../workspaceTree"
import { BoardComponent } from "./boardComponent" import { BoardComponent } from "./boardComponent"
@ -12,16 +12,20 @@ type Props = {
mutator: Mutator, mutator: Mutator,
workspaceTree: WorkspaceTree workspaceTree: WorkspaceTree
boardTree?: BoardTree 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<Props> { class WorkspaceComponent extends React.Component<Props> {
render() { render() {
const { mutator, boardTree, workspaceTree, pageController } = this.props const { mutator, boardTree, workspaceTree, showBoard} = this.props
const element = const element =
<div className="octo-workspace"> <div className="octo-workspace">
<Sidebar mutator={mutator} pageController={pageController} workspaceTree={workspaceTree} boardTree={boardTree}></Sidebar> <Sidebar mutator={mutator} showBoard={showBoard} workspaceTree={workspaceTree} boardTree={boardTree}></Sidebar>
{this.mainComponent()} {this.mainComponent()}
</div> </div>
@ -29,7 +33,7 @@ class WorkspaceComponent extends React.Component<Props> {
} }
private mainComponent() { private mainComponent() {
const { mutator, boardTree, pageController } = this.props const { mutator, boardTree, showCard, showFilter, setSearchText, showView } = this.props
const { activeView } = boardTree || {} const { activeView } = boardTree || {}
if (!activeView) { if (!activeView) {
@ -38,11 +42,11 @@ class WorkspaceComponent extends React.Component<Props> {
switch (activeView?.viewType) { switch (activeView?.viewType) {
case "board": { case "board": {
return <BoardComponent mutator={mutator} boardTree={boardTree} pageController={pageController} /> return <BoardComponent mutator={mutator} boardTree={boardTree} showCard={showCard} showFilter={showFilter} setSearchText={setSearchText} showView={showView}/>
} }
case "table": { case "table": {
return <TableComponent mutator={mutator} boardTree={boardTree} pageController={pageController} /> return <TableComponent mutator={mutator} boardTree={boardTree} showCard={showCard} showFilter={showFilter} setSearchText={setSearchText} showView={showView}/>
} }
default: { default: {

6
src/client/main.tsx Normal file
View file

@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
ReactDOM.render(<App />, document.getElementById('octo-tasks-app'));

View file

@ -9,7 +9,7 @@ import { IBlock, IPageController } from "./octoTypes"
import { Utils } from "./utils" import { Utils } from "./utils"
class OctoUtils { 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 { board } = boardTree
const options: MenuOption[] = boardTree.views.map(view => ({ id: view.id, name: view.title || "Untitled View" })) 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 view = boardTree.activeView
const nextView = boardTree.views.find(o => o !== view) const nextView = boardTree.views.find(o => o !== view)
await mutator.deleteBlock(view, "delete view") await mutator.deleteBlock(view, "delete view")
pageController.showView(nextView.id) showView(nextView.id)
break break
} }
case "__addview-board": { case "__addview-board": {
@ -48,8 +48,8 @@ class OctoUtils {
await mutator.insertBlock( await mutator.insertBlock(
view, view,
"add view", "add view",
async () => { pageController.showView(view.id) }, async () => { showView(view.id) },
async () => { pageController.showView(oldViewId) }) async () => { showView(oldViewId) })
break break
} }
case "__addview-table": { case "__addview-table": {
@ -65,13 +65,13 @@ class OctoUtils {
await mutator.insertBlock( await mutator.insertBlock(
view, view,
"add view", "add view",
async () => { pageController.showView(view.id) }, async () => { showView(view.id) },
async () => { pageController.showView(oldViewId) }) async () => { showView(oldViewId) })
break break
} }
default: { default: {
const view = boardTree.views.find(o => o.id === optionId) const view = boardTree.views.find(o => o.id === optionId)
pageController.showView(view.id) showView(view.id)
} }
} }
} }

View file

@ -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<Props, State> {
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(
<CardDialog mutator={mutator} boardTree={this.state.boardTree} cardTree={this.shownCardTree} onClose={() => { this.showCard(undefined) }}></CardDialog>,
Utils.getElementById("overlay")
)
} else {
ReactDOM.render(
<div />,
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(
<FilterComponent
mutator={mutator}
boardTree={this.state.boardTree}
pageX={pageX}
pageY={pageY}
onClose={() => { this.showFilter(undefined) }}
>
</FilterComponent>,
Utils.getElementById("modal")
)
} else {
ReactDOM.render(<div />, Utils.getElementById("modal"))
}
return (
<div className='BoardPage'>
<WorkspaceComponent mutator={mutator} workspaceTree={this.workspaceTree} boardTree={this.state.boardTree} showView={this.showView} showCard={this.showCard} showBoard={this.showBoard} showFilter={this.showFilter} setSearchText={this.setSearchText} />,
</div>
);
}
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)
}
}

View file

@ -0,0 +1,76 @@
import React from 'react';
import { IBlock } from "../octoTypes"
import { Archiver } from "../archiver"
import { Board } from "../board"
import { Mutator } from "../mutator"
import { OctoClient } from "../octoClient"
import { UndoManager } from "../undomanager"
import { Utils } from "../utils"
import Button from '../components/button';
type Props = {};
type State = {
boards: IBlock[];
};
export default class HomePage extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
boards: [],
}
}
componentDidMount() {
this.loadBoards();
}
loadBoards = async () => {
const octo = new OctoClient()
const boards = await octo.getBlocks(null, "board")
this.setState({boards});
}
importClicked = async () => {
const octo = new OctoClient()
const mutator = new Mutator(octo, UndoManager.shared)
Archiver.importFullArchive(mutator, () => {
this.loadBoards()
})
}
exportClicked = async () => {
const octo = new OctoClient()
const mutator = new Mutator(octo, UndoManager.shared)
Archiver.exportFullArchive(mutator)
}
addClicked = async () => {
const octo = new OctoClient()
const board = new Board()
await octo.insertBlock(board)
}
public render(): React.ReactNode {
return (
<div>
<Button onClick={this.addClicked}>+ Add Board</Button>
<br />
<Button onClick={this.addClicked}>Import Archive</Button>
<br />
<Button onClick={this.addClicked}>Export Archive</Button>
{this.state.boards.map((board) => (
<p>
<a href={`/board/${board.id}`}>
{board.icon && <span>{board.icon}</span>}
<span>{board.title}</span>
<span>{Utils.displayDate(new Date(board.updateAt))}</span>
</a>
</p>
))}
</div>
);
}
}

View file

@ -0,0 +1,37 @@
import React from "react"
type Props = {}
type State = {
username: string;
password: string;
}
export default class LoginPage extends React.Component<Props, State> {
state = {
username: '',
password: '',
}
handleLogin = () => {
console.log("Logging in");
}
public render(): React.ReactNode {
return (
<div className='LoginPage'>
<label htmlFor='login-username'>Username</label>
<input
id='login-username'
value={this.state.username}
onChange={(e) => this.setState({username: e.target.value})}
/>
<label htmlFor='login-username'>Password</label>
<input
id='login-password'
/>
<button onClick={this.handleLogin}>Login</button>
</div>
)
}
}

View file

@ -50,13 +50,13 @@ function makeCommonConfig() {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
inject: true, inject: true,
title: "OCTO", title: "OCTO",
chunks: ["boardPage"], chunks: ["main"],
template: "html-templates/page.ejs", template: "html-templates/page.ejs",
filename: 'board.html' filename: 'index.html'
}), }),
], ],
entry: { entry: {
boardPage: "./src/client/boardPage.tsx" main: "./src/client/main.tsx",
}, },
output: { output: {
filename: "[name].js", filename: "[name].js",