mirror of
https://github.com/mattermost/focalboard.git
synced 2025-05-31 22:24:57 +02:00
Merge branch 'main' into menu-widget
This commit is contained in:
commit
b202277772
@ -14,6 +14,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="octo-tasks-app">
|
<div id="octo-tasks-app">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="root-portal">
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
16095
package-lock.json
generated
16095
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
19
package.json
19
package.json
@ -6,22 +6,39 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"pack": "NODE_ENV=production webpack --config webpack.js",
|
"pack": "NODE_ENV=production webpack --config webpack.js",
|
||||||
"packdev": "NODE_ENV=dev webpack --config webpack.dev.js",
|
"packdev": "NODE_ENV=dev webpack --config webpack.dev.js",
|
||||||
"watchdev": "NODE_ENV=dev webpack --watch --config webpack.dev.js"
|
"watchdev": "NODE_ENV=dev webpack --watch --config webpack.dev.js",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"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"
|
||||||
},
|
},
|
||||||
|
"jest": {
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.tsx?$": "ts-jest"
|
||||||
|
}
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^5.11.4",
|
||||||
|
"@testing-library/react": "^11.0.4",
|
||||||
|
"@types/jest": "^26.0.14",
|
||||||
"@types/marked": "^1.1.0",
|
"@types/marked": "^1.1.0",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
|
"@types/react-router-dom": "^5.1.6",
|
||||||
"copy-webpack-plugin": "^6.0.3",
|
"copy-webpack-plugin": "^6.0.3",
|
||||||
|
"css-loader": "^4.3.0",
|
||||||
"file-loader": "^6.1.0",
|
"file-loader": "^6.1.0",
|
||||||
"html-webpack-plugin": "^4.5.0",
|
"html-webpack-plugin": "^4.5.0",
|
||||||
|
"jest": "^26.5.3",
|
||||||
|
"sass": "^1.27.0",
|
||||||
|
"sass-loader": "^10.0.2",
|
||||||
|
"style-loader": "^1.3.0",
|
||||||
"terser-webpack-plugin": "^4.1.0",
|
"terser-webpack-plugin": "^4.1.0",
|
||||||
|
"ts-jest": "^26.4.1",
|
||||||
"ts-loader": "^8.0.3",
|
"ts-loader": "^8.0.3",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.0.2",
|
||||||
"webpack": "^4.44.1",
|
"webpack": "^4.44.1",
|
||||||
|
@ -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")
|
||||||
|
37
src/client/app.tsx
Normal file
37
src/client/app.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
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 id="frame">
|
||||||
|
<div className="page-header">
|
||||||
|
<a href="/">OCTO</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="main">
|
||||||
|
<Switch>
|
||||||
|
<Route path="/login">
|
||||||
|
<LoginPage />
|
||||||
|
</Route>
|
||||||
|
<Route path="/board">
|
||||||
|
<BoardPage />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="overlay">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Router>
|
||||||
|
)
|
||||||
|
}
|
@ -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")
|
|
13
src/client/components/__snapshots__/rootPortal.test.tsx.snap
Normal file
13
src/client/components/__snapshots__/rootPortal.test.tsx.snap
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`components/RootPortal should match snapshot 1`] = `
|
||||||
|
<div
|
||||||
|
id="root-portal"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
Testing Portal
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -9,26 +9,29 @@ import { Constants } from "../constants"
|
|||||||
import ViewMenu from "../components/viewMenu"
|
import ViewMenu from "../components/viewMenu"
|
||||||
import { Menu as OldMenu } from "../menu"
|
import { Menu as OldMenu } 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 { Board } from "../board"
|
import { Board } from "../board"
|
||||||
import { BoardView } from "../boardView"
|
import { BoardView } from "../boardView"
|
||||||
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 = {
|
||||||
isHoverOnCover: boolean
|
isHoverOnCover: boolean
|
||||||
isSearching: boolean
|
isSearching: boolean
|
||||||
viewMenu: boolean
|
viewMenu: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
class BoardComponent extends React.Component<Props, State> {
|
class BoardComponent extends React.Component<Props, State> {
|
||||||
@ -38,7 +41,7 @@ class BoardComponent extends React.Component<Props, State> {
|
|||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText(), viewMenu: false}
|
this.state = { isHoverOnCover: false, isSearching: !!this.props.boardTree?.getSearchText(), viewMenu: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevPros: Props, prevState: State) {
|
componentDidUpdate(prevPros: Props, prevState: State) {
|
||||||
@ -48,7 +51,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 (
|
||||||
@ -92,21 +95,21 @@ 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
|
<div
|
||||||
className="octo-button"
|
className="octo-button"
|
||||||
style={{ color: "#000000", fontWeight: 600 }}
|
style={{ color: "#000000", fontWeight: 600 }}
|
||||||
onClick={() => this.setState({viewMenu: true})}
|
onClick={() => this.setState({ viewMenu: true })}
|
||||||
>
|
>
|
||||||
{this.state.viewMenu &&
|
{this.state.viewMenu &&
|
||||||
<ViewMenu
|
<ViewMenu
|
||||||
board={board}
|
board={board}
|
||||||
onClose={() => this.setState({viewMenu:false})}
|
onClose={() => this.setState({ viewMenu: false })}
|
||||||
mutator={mutator}
|
mutator={mutator}
|
||||||
boardTree={boardTree}
|
boardTree={boardTree}
|
||||||
pageController={pageController}
|
pageController={pageController}
|
||||||
/>}
|
/>}
|
||||||
<div className="imageDropdown"></div>
|
<div className="imageDropdown"></div>
|
||||||
</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) }}>
|
||||||
@ -241,7 +244,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) {
|
||||||
@ -288,8 +291,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) {
|
||||||
@ -421,13 +423,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
17
src/client/components/button.scss
Normal file
17
src/client/components/button.scss
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
.Button {
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0 5px;
|
||||||
|
min-width: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: background 100ms ease-out 0s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eeeeee;
|
||||||
|
}
|
||||||
|
.octo-hovercontrol {
|
||||||
|
background: rgb(239, 239, 238);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
|
||||||
|
import './button.scss'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
@ -8,13 +10,13 @@ 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 (
|
||||||
<div
|
<div
|
||||||
onClick={this.props.onClick}
|
onClick={this.props.onClick}
|
||||||
className="octo-button"
|
className="Button octo-button"
|
||||||
style={style}
|
style={style}
|
||||||
title={this.props.title}>
|
title={this.props.title}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
@ -22,5 +24,3 @@ class Button extends React.Component<Props> {
|
|||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Button }
|
|
||||||
|
@ -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"
|
||||||
|
|
||||||
|
30
src/client/components/rootPortal.test.tsx
Normal file
30
src/client/components/rootPortal.test.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) 2020-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import {render} from '@testing-library/react';
|
||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
import RootPortal from './rootPortal';
|
||||||
|
|
||||||
|
describe('components/RootPortal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Quick fix to disregard console error when unmounting a component
|
||||||
|
console.error = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should match snapshot', () => {
|
||||||
|
const rootPortalDiv = document.createElement('div');
|
||||||
|
rootPortalDiv.id = 'root-portal';
|
||||||
|
|
||||||
|
const {getByText, container} = render(
|
||||||
|
<RootPortal>
|
||||||
|
<div>{'Testing Portal'}</div>
|
||||||
|
</RootPortal>,
|
||||||
|
{container: document.body.appendChild(rootPortalDiv)},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getByText('Testing Portal')).toBeVisible();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
44
src/client/components/rootPortal.tsx
Normal file
44
src/client/components/rootPortal.tsx
Normal file
@ -0,0 +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';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RootPortal extends React.PureComponent<Props> {
|
||||||
|
el: HTMLDivElement
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.el = document.createElement('div');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
this.props.children,
|
||||||
|
this.el,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
@ -18,6 +18,10 @@ class Sidebar extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { workspaceTree } = this.props
|
const { workspaceTree } = this.props
|
||||||
|
if (!workspaceTree) {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
const { boards } = workspaceTree
|
const { boards } = workspaceTree
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -47,7 +51,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 +68,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 +108,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)
|
||||||
}
|
}
|
||||||
|
@ -8,17 +8,20 @@ import { CsvExporter } from "../csvExporter"
|
|||||||
import ViewMenu from "../components/viewMenu"
|
import ViewMenu from "../components/viewMenu"
|
||||||
import { Menu as OldMenu } from "../menu"
|
import { Menu as OldMenu } 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 = {
|
||||||
@ -45,7 +48,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 (
|
||||||
@ -104,8 +107,8 @@ class TableComponent extends React.Component<Props, State> {
|
|||||||
</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>
|
||||||
<div className={ hasSort ? "octo-button active" : "octo-button"} onClick={(e) => { OctoUtils.showSortMenu(e, mutator, boardTree) }}>Sort</div>
|
<div className={hasSort ? "octo-button active" : "octo-button"} onClick={(e) => { OctoUtils.showSortMenu(e, mutator, boardTree) }}>Sort</div>
|
||||||
{this.state.isSearching
|
{this.state.isSearching
|
||||||
? <Editable
|
? <Editable
|
||||||
ref={this.searchFieldRef}
|
ref={this.searchFieldRef}
|
||||||
@ -259,8 +262,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) {
|
||||||
@ -364,7 +366,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) {
|
||||||
@ -412,13 +414,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,21 @@ 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
|
||||||
|
|
||||||
|
Utils.assert(workspaceTree)
|
||||||
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 +34,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 +43,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
6
src/client/main.tsx
Normal 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'));
|
@ -5,7 +5,7 @@ import { BoardView, ISortOption } from "./boardView"
|
|||||||
import { Editable } from "./components/editable"
|
import { Editable } from "./components/editable"
|
||||||
import { Menu, MenuOption } from "./menu"
|
import { Menu, MenuOption } from "./menu"
|
||||||
import { Mutator } from "./mutator"
|
import { Mutator } from "./mutator"
|
||||||
import { IBlock, IPageController } from "./octoTypes"
|
import { IBlock } from "./octoTypes"
|
||||||
import { Utils } from "./utils"
|
import { Utils } from "./utils"
|
||||||
|
|
||||||
class OctoUtils {
|
class OctoUtils {
|
||||||
|
256
src/client/pages/boardPage.tsx
Normal file
256
src/client/pages/boardPage.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
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 { WorkspaceComponent } from "../components/workspaceComponent"
|
||||||
|
import { FlashMessage } from "../flashMessage"
|
||||||
|
import { Mutator } from "../mutator"
|
||||||
|
import { OctoClient } from "../octoClient"
|
||||||
|
import { OctoListener } from "../octoListener"
|
||||||
|
import { IBlock } 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
|
||||||
|
shownCardTree?: CardTree
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class BoardPage extends React.Component<Props, State> {
|
||||||
|
view: BoardView
|
||||||
|
|
||||||
|
updateTitleTimeout: number
|
||||||
|
updatePropertyLabelTimeout: number
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.log(`BoardPage. boardId: ${boardId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props, prevState: State) {
|
||||||
|
Utils.log(`componentDidUpdate`)
|
||||||
|
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.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`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 { workspaceTree, shownCardTree } = this.state
|
||||||
|
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 && shownCardTree) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<CardDialog mutator={mutator} boardTree={this.state.boardTree} cardTree={shownCardTree} onClose={() => { this.showCard(undefined) }}></CardDialog>,
|
||||||
|
Utils.getElementById("overlay")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
const overlay = document.getElementById("overlay")
|
||||||
|
if (overlay) {
|
||||||
|
ReactDOM.render(
|
||||||
|
<div />,
|
||||||
|
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 {
|
||||||
|
const modal = document.getElementById("modal")
|
||||||
|
if (modal) {
|
||||||
|
ReactDOM.render(<div />, modal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.log(`BoardPage.render ${this.state.boardTree?.board?.title}`)
|
||||||
|
return (
|
||||||
|
<div className='BoardPage'>
|
||||||
|
<WorkspaceComponent
|
||||||
|
mutator={mutator}
|
||||||
|
workspaceTree={workspaceTree}
|
||||||
|
boardTree={this.state.boardTree}
|
||||||
|
showView={(id) => { this.showView(id) }}
|
||||||
|
showCard={(card) => { this.showCard(card) }}
|
||||||
|
showBoard={(id) => { this.showBoard(id) }}
|
||||||
|
showFilter={(el) => { this.showFilter(el) }}
|
||||||
|
setSearchText={(text) => { this.setSearchText(text) }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async attachToBoard(boardId: string, viewId?: string) {
|
||||||
|
Utils.log(`attachToBoard: ${boardId}`)
|
||||||
|
|
||||||
|
this.boardListener.open(boardId, (blockId: string) => {
|
||||||
|
console.log(`octoListener.onChanged: ${blockId}`)
|
||||||
|
this.sync(boardId)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sync(boardId, viewId)
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync(boardId: string = this.state.boardId, viewId: string | undefined = this.state.viewId) {
|
||||||
|
const { workspaceTree } = this.state
|
||||||
|
Utils.log(`sync start: ${boardId}`)
|
||||||
|
|
||||||
|
await workspaceTree.sync()
|
||||||
|
|
||||||
|
if (boardId) {
|
||||||
|
const boardTree = new BoardTree(this.octo, boardId)
|
||||||
|
await boardTree.sync()
|
||||||
|
|
||||||
|
// Default to first view
|
||||||
|
if (!viewId) {
|
||||||
|
viewId = boardTree.views[0].id
|
||||||
|
}
|
||||||
|
|
||||||
|
boardTree.setActiveView(viewId)
|
||||||
|
// TODO: Handle error (viewId not found)
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
boardTree,
|
||||||
|
viewId: boardTree.activeView.id
|
||||||
|
})
|
||||||
|
Utils.log(`sync complete: ${boardTree.board.id} (${boardTree.board.title})`)
|
||||||
|
} else {
|
||||||
|
this.forceUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPageController
|
||||||
|
|
||||||
|
async showCard(card: IBlock) {
|
||||||
|
this.cardListener.close()
|
||||||
|
|
||||||
|
if (card) {
|
||||||
|
const cardTree = new CardTree(this.octo, card.id)
|
||||||
|
await cardTree.sync()
|
||||||
|
this.setState({...this.state, shownCardTree: cardTree})
|
||||||
|
|
||||||
|
this.cardListener = new OctoListener()
|
||||||
|
this.cardListener.open(card.id, async () => {
|
||||||
|
await cardTree.sync()
|
||||||
|
this.forceUpdate()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.setState({...this.state, shownCardTree: undefined})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showBoard(boardId: string) {
|
||||||
|
const { boardTree } = this.state
|
||||||
|
|
||||||
|
if (boardTree?.board?.id === boardId) { return }
|
||||||
|
|
||||||
|
const newUrl = window.location.protocol + "//" + window.location.host + window.location.pathname + `?id=${encodeURIComponent(boardId)}`
|
||||||
|
window.history.pushState({ path: newUrl }, "", newUrl)
|
||||||
|
|
||||||
|
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.state.boardTree?.setSearchText(text)
|
||||||
|
}
|
||||||
|
}
|
76
src/client/pages/homePage.tsx
Normal file
76
src/client/pages/homePage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
src/client/pages/loginPage.tsx
Normal file
37
src/client/pages/loginPage.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -73,7 +73,9 @@ hr {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
#octo-tasks-app > #main {
|
#octo-tasks-app #frame,
|
||||||
|
#octo-tasks-app #main,
|
||||||
|
#octo-tasks-app .BoardPage {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -26,6 +26,14 @@ function makeCommonConfig() {
|
|||||||
loader: "file-loader",
|
loader: "file-loader",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
test: /\.s[ac]ss$/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
'css-loader',
|
||||||
|
'sass-loader',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
test: /\.(tsx?|js|jsx|html)$/,
|
test: /\.(tsx?|js|jsx|html)$/,
|
||||||
use: [
|
use: [
|
||||||
],
|
],
|
||||||
@ -50,13 +58,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",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user