Initial implementation of sidebar.
This commit is contained in:
parent
e227d384d2
commit
a84ff6901a
14 changed files with 417 additions and 257 deletions
|
@ -1,29 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="/main.css">
|
|
||||||
<link rel="stylesheet" href="/images.css">
|
|
||||||
<link rel="stylesheet" href="/colors.css">
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.location.href = "/boards"
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="container">
|
|
||||||
<header id="header">
|
|
||||||
<a href="/">OCTO</a>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main id="main">
|
|
||||||
<p>
|
|
||||||
<a href="boards">All Boards</a>
|
|
||||||
</p>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
|
@ -39,6 +39,13 @@ func handleStaticFile(r *mux.Router, requestPath string, filePath string, conten
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleDefault(r *mux.Router, requestPath string) {
|
||||||
|
r.HandleFunc(requestPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Printf("handleDefault")
|
||||||
|
http.Redirect(w, r, "/board", http.StatusFound)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------------------------------
|
||||||
// REST APIs
|
// REST APIs
|
||||||
|
|
||||||
|
@ -48,12 +55,15 @@ func handleGetBlocks(w http.ResponseWriter, r *http.Request) {
|
||||||
blockType := query.Get("type")
|
blockType := query.Get("type")
|
||||||
|
|
||||||
var blocks []string
|
var blocks []string
|
||||||
if len(blockType) > 0 {
|
if len(blockType) > 0 && len(parentID) > 0 {
|
||||||
blocks = getBlocksWithParentAndType(parentID, blockType)
|
blocks = getBlocksWithParentAndType(parentID, blockType)
|
||||||
|
} else if len(blockType) > 0 {
|
||||||
|
blocks = getBlocksWithType(blockType)
|
||||||
} else {
|
} else {
|
||||||
blocks = getBlocksWithParent(parentID)
|
blocks = getBlocksWithParent(parentID)
|
||||||
}
|
}
|
||||||
log.Printf("GetBlocks parentID: %s, %d result(s)", parentID, len(blocks))
|
|
||||||
|
log.Printf("GetBlocks parentID: %s, type: %s, %d result(s)", parentID, blockType, len(blocks))
|
||||||
response := `[` + strings.Join(blocks[:], ",") + `]`
|
response := `[` + strings.Join(blocks[:], ",") + `]`
|
||||||
jsonResponse(w, 200, response)
|
jsonResponse(w, 200, response)
|
||||||
}
|
}
|
||||||
|
@ -335,11 +345,9 @@ func main() {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
handleStaticFile(r, "/", "index.html", "text/html; charset=utf-8")
|
handleDefault(r, "/")
|
||||||
handleStaticFile(r, "/boards", "boards.html", "text/html; charset=utf-8")
|
|
||||||
handleStaticFile(r, "/board", "board.html", "text/html; charset=utf-8")
|
|
||||||
|
|
||||||
handleStaticFile(r, "/boardsPage.js", "boardsPage.js", "text/javascript; charset=utf-8")
|
handleStaticFile(r, "/board", "board.html", "text/html; 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")
|
||||||
|
|
|
@ -155,6 +155,32 @@ func getBlocksWithParent(parentID string) []string {
|
||||||
return blocksFromRows(rows)
|
return blocksFromRows(rows)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getBlocksWithType(blockType string) []string {
|
||||||
|
query := `WITH latest AS
|
||||||
|
(
|
||||||
|
SELECT * FROM
|
||||||
|
(
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY id ORDER BY insert_at DESC) AS rn
|
||||||
|
FROM blocks
|
||||||
|
) a
|
||||||
|
WHERE rn = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
SELECT COALESCE("json", '{}')
|
||||||
|
FROM latest
|
||||||
|
WHERE delete_at = 0 and type = $1`
|
||||||
|
|
||||||
|
rows, err := db.Query(query, blockType)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(`getBlocksWithParentAndType ERROR: %v`, err)
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return blocksFromRows(rows)
|
||||||
|
}
|
||||||
|
|
||||||
func getSubTree(blockID string) []string {
|
func getSubTree(blockID string) []string {
|
||||||
query := `WITH latest AS
|
query := `WITH latest AS
|
||||||
(
|
(
|
||||||
|
|
|
@ -3,11 +3,10 @@ import ReactDOM from "react-dom"
|
||||||
import { BoardTree } from "./boardTree"
|
import { BoardTree } from "./boardTree"
|
||||||
import { BoardView } from "./boardView"
|
import { BoardView } from "./boardView"
|
||||||
import { CardTree } from "./cardTree"
|
import { CardTree } from "./cardTree"
|
||||||
import { BoardComponent } from "./components/boardComponent"
|
|
||||||
import { CardDialog } from "./components/cardDialog"
|
import { CardDialog } from "./components/cardDialog"
|
||||||
import { FilterComponent } from "./components/filterComponent"
|
import { FilterComponent } from "./components/filterComponent"
|
||||||
import { PageHeader } from "./components/pageHeader"
|
import { PageHeader } from "./components/pageHeader"
|
||||||
import { TableComponent } from "./components/tableComponent"
|
import { WorkspaceComponent } from "./components/workspaceComponent"
|
||||||
import { FlashMessage } from "./flashMessage"
|
import { FlashMessage } from "./flashMessage"
|
||||||
import { Mutator } from "./mutator"
|
import { Mutator } from "./mutator"
|
||||||
import { OctoClient } from "./octoClient"
|
import { OctoClient } from "./octoClient"
|
||||||
|
@ -15,6 +14,7 @@ import { OctoListener } from "./octoListener"
|
||||||
import { IBlock, IPageController } from "./octoTypes"
|
import { IBlock, IPageController } from "./octoTypes"
|
||||||
import { UndoManager } from "./undomanager"
|
import { UndoManager } from "./undomanager"
|
||||||
import { Utils } from "./utils"
|
import { Utils } from "./utils"
|
||||||
|
import { WorkspaceTree } from "./workspaceTree"
|
||||||
|
|
||||||
class BoardPage implements IPageController {
|
class BoardPage implements IPageController {
|
||||||
boardTitle: HTMLElement
|
boardTitle: HTMLElement
|
||||||
|
@ -23,10 +23,11 @@ class BoardPage implements IPageController {
|
||||||
groupByButton: HTMLElement
|
groupByButton: HTMLElement
|
||||||
groupByLabel: HTMLElement
|
groupByLabel: HTMLElement
|
||||||
|
|
||||||
boardId: string
|
boardId?: string
|
||||||
viewId: string
|
viewId?: string
|
||||||
|
|
||||||
boardTree: BoardTree
|
workspaceTree: WorkspaceTree
|
||||||
|
boardTree?: BoardTree
|
||||||
view: BoardView
|
view: BoardView
|
||||||
|
|
||||||
updateTitleTimeout: number
|
updateTitleTimeout: number
|
||||||
|
@ -41,28 +42,18 @@ class BoardPage implements IPageController {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const queryString = new URLSearchParams(window.location.search)
|
const queryString = new URLSearchParams(window.location.search)
|
||||||
if (!queryString.has("id")) {
|
const boardId = queryString.get("id")
|
||||||
// No id, redirect to home
|
const viewId = queryString.get("v")
|
||||||
window.location.href = "/"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
this.layoutPage()
|
this.layoutPage()
|
||||||
|
|
||||||
this.boardId = queryString.get("id")
|
this.workspaceTree = new WorkspaceTree(this.octo)
|
||||||
this.viewId = queryString.get("v")
|
|
||||||
|
|
||||||
console.log(`BoardPage. boardId: ${this.boardId}`)
|
console.log(`BoardPage. boardId: ${this.boardId}`)
|
||||||
if (this.boardId) {
|
if (boardId) {
|
||||||
this.boardTree = new BoardTree(this.octo, this.boardId)
|
this.attachToBoard(boardId, viewId)
|
||||||
this.sync()
|
|
||||||
|
|
||||||
this.boardListener.open(this.boardId, (blockId: string) => {
|
|
||||||
console.log(`octoListener.onChanged: ${blockId}`)
|
|
||||||
this.sync()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
// Show error
|
this.sync()
|
||||||
}
|
}
|
||||||
|
|
||||||
document.body.addEventListener("keydown", async (e) => {
|
document.body.addEventListener("keydown", async (e) => {
|
||||||
|
@ -111,10 +102,15 @@ class BoardPage implements IPageController {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { octo, boardTree } = this
|
const { octo, boardTree } = this
|
||||||
const { board, activeView } = boardTree
|
const { board, activeView } = boardTree || {}
|
||||||
const mutator = new Mutator(octo)
|
const mutator = new Mutator(octo)
|
||||||
|
|
||||||
const rootElement = Utils.getElementById("main")
|
const mainElement = Utils.getElementById("main")
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<PageHeader />,
|
||||||
|
Utils.getElementById("header")
|
||||||
|
)
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<PageHeader />,
|
<PageHeader />,
|
||||||
|
@ -123,54 +119,23 @@ class BoardPage implements IPageController {
|
||||||
|
|
||||||
if (board) {
|
if (board) {
|
||||||
Utils.setFavicon(board.icon)
|
Utils.setFavicon(board.icon)
|
||||||
} else {
|
document.title = `OCTO - ${board.title} | ${activeView.title}`
|
||||||
ReactDOM.render(
|
|
||||||
<div className="page-loading">Loading...</div>,
|
|
||||||
rootElement
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeView) {
|
ReactDOM.render(
|
||||||
document.title = `OCTO - ${board.title} | ${activeView.title}`
|
<WorkspaceComponent mutator={mutator} workspaceTree={this.workspaceTree} boardTree={this.boardTree} pageController={this} />,
|
||||||
|
mainElement
|
||||||
|
)
|
||||||
|
|
||||||
switch (activeView.viewType) {
|
if (boardTree && boardTree.board && this.shownCardTree) {
|
||||||
case "board": {
|
ReactDOM.render(
|
||||||
ReactDOM.render(
|
<CardDialog mutator={mutator} boardTree={boardTree} cardTree={this.shownCardTree} onClose={() => { this.showCard(undefined) }}></CardDialog>,
|
||||||
<BoardComponent mutator={mutator} boardTree={this.boardTree} pageController={this} />,
|
Utils.getElementById("overlay")
|
||||||
rootElement
|
)
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case "table": {
|
|
||||||
ReactDOM.render(
|
|
||||||
<TableComponent mutator={mutator} boardTree={this.boardTree} pageController={this} />,
|
|
||||||
rootElement
|
|
||||||
)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
Utils.assertFailure(`render() Unhandled viewType: ${activeView.viewType}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<div>Loading...</div>,
|
<div />,
|
||||||
rootElement
|
Utils.getElementById("overlay")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +154,7 @@ class BoardPage implements IPageController {
|
||||||
boardTree={boardTree}
|
boardTree={boardTree}
|
||||||
pageX={pageX}
|
pageX={pageX}
|
||||||
pageY={pageY}
|
pageY={pageY}
|
||||||
onClose={() => {this.showFilter(undefined)}}
|
onClose={() => { this.showFilter(undefined) }}
|
||||||
>
|
>
|
||||||
</FilterComponent>,
|
</FilterComponent>,
|
||||||
Utils.getElementById("modal")
|
Utils.getElementById("modal")
|
||||||
|
@ -199,22 +164,38 @@ class BoardPage implements IPageController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
async sync() {
|
||||||
const { boardTree } = this
|
const { workspaceTree, boardTree } = this
|
||||||
|
|
||||||
await boardTree.sync()
|
await workspaceTree.sync()
|
||||||
|
if (boardTree) {
|
||||||
|
await boardTree.sync()
|
||||||
|
|
||||||
// Default to first view
|
// Default to first view
|
||||||
if (!this.viewId) {
|
if (!this.viewId) {
|
||||||
this.viewId = boardTree.views[0].id
|
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}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
this.render()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,6 +221,15 @@ class BoardPage implements IPageController {
|
||||||
this.render()
|
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) {
|
showView(viewId: string) {
|
||||||
this.viewId = viewId
|
this.viewId = viewId
|
||||||
this.boardTree.setActiveView(this.viewId)
|
this.boardTree.setActiveView(this.viewId)
|
||||||
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { Archiver } from "./archiver"
|
|
||||||
import { Board } from "./board"
|
|
||||||
import { Mutator } from "./mutator"
|
|
||||||
import { OctoClient } from "./octoClient"
|
|
||||||
import { UndoManager } from "./undomanager"
|
|
||||||
import { Utils } from "./utils"
|
|
||||||
|
|
||||||
class BoardsPage {
|
|
||||||
boardsPanel: HTMLElement
|
|
||||||
|
|
||||||
boardId: string
|
|
||||||
boards: Board[]
|
|
||||||
|
|
||||||
octo = new OctoClient()
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
// This is a placeholder page
|
|
||||||
|
|
||||||
const root = Utils.getElementById("octo-tasks-app")
|
|
||||||
root.innerText = ""
|
|
||||||
|
|
||||||
// Header
|
|
||||||
root.appendChild(Utils.htmlToElement(`<div class="page-header"><a href="/">OCTO</a></div`))
|
|
||||||
|
|
||||||
const mainPanel = root.appendChild(document.createElement("div"))
|
|
||||||
|
|
||||||
this.boardsPanel = mainPanel.appendChild(document.createElement("div"))
|
|
||||||
|
|
||||||
{
|
|
||||||
const addButton = document.body.appendChild(document.createElement("div"))
|
|
||||||
addButton.className = "octo-button"
|
|
||||||
addButton.innerText = "+ Add Board"
|
|
||||||
addButton.onclick = () => { this.addClicked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(document.createElement("br"))
|
|
||||||
|
|
||||||
{
|
|
||||||
const importButton = document.body.appendChild(document.createElement("div"))
|
|
||||||
importButton.className = "octo-button"
|
|
||||||
importButton.innerText = "Import archive"
|
|
||||||
importButton.onclick = async () => {
|
|
||||||
const octo = new OctoClient()
|
|
||||||
const mutator = new Mutator(octo, UndoManager.shared)
|
|
||||||
Archiver.importFullArchive(mutator, () => {
|
|
||||||
this.updateView()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const exportButton = document.body.appendChild(document.createElement("div"))
|
|
||||||
exportButton.className = "octo-button"
|
|
||||||
exportButton.innerText = "Export archive"
|
|
||||||
exportButton.onclick = () => {
|
|
||||||
const octo = new OctoClient()
|
|
||||||
const mutator = new Mutator(octo, UndoManager.shared)
|
|
||||||
Archiver.exportFullArchive(mutator)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateView()
|
|
||||||
}
|
|
||||||
|
|
||||||
async getBoardData() {
|
|
||||||
const boards = this.octo.getBlocks(null, "board")
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateView() {
|
|
||||||
const { boardsPanel } = this
|
|
||||||
|
|
||||||
boardsPanel.innerText = ""
|
|
||||||
|
|
||||||
const boards = await this.octo.getBlocks(null, "board")
|
|
||||||
for (const board of boards) {
|
|
||||||
const p = boardsPanel.appendChild(document.createElement("p"))
|
|
||||||
const a = p.appendChild(document.createElement("a"))
|
|
||||||
a.style.padding = "5px 10px"
|
|
||||||
a.style.fontSize = "20px"
|
|
||||||
a.href = `./board?id=${encodeURIComponent(board.id)}`
|
|
||||||
|
|
||||||
if (board.icon) {
|
|
||||||
const icon = a.appendChild(document.createElement("span"))
|
|
||||||
icon.className = "octo-icon"
|
|
||||||
icon.style.marginRight = "10px"
|
|
||||||
icon.innerText = board.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = a.appendChild(document.createElement("b"))
|
|
||||||
const updatedDate = new Date(board.updateAt)
|
|
||||||
title.innerText = board.title
|
|
||||||
const details = a.appendChild(document.createElement("span"))
|
|
||||||
details.style.fontSize = "15px"
|
|
||||||
details.style.color = "#909090"
|
|
||||||
details.style.marginLeft = "10px"
|
|
||||||
details.innerText = ` ${Utils.displayDate(updatedDate)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`updateView: ${boards.length} board(s).`)
|
|
||||||
}
|
|
||||||
|
|
||||||
async addClicked() {
|
|
||||||
const board = new Board()
|
|
||||||
await this.octo.insertBlock(board)
|
|
||||||
await this.updateView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export = BoardsPage
|
|
||||||
|
|
||||||
const _ = new BoardsPage()
|
|
||||||
console.log("boardsView")
|
|
126
src/client/components/sidebar.tsx
Normal file
126
src/client/components/sidebar.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import React from "react"
|
||||||
|
import { Archiver } from "../archiver"
|
||||||
|
import { Board } from "../board"
|
||||||
|
import { BoardTree } from "../boardTree"
|
||||||
|
import { Menu, MenuOption } from "../menu"
|
||||||
|
import { Mutator } from "../mutator"
|
||||||
|
import { IPageController } from "../octoTypes"
|
||||||
|
import { WorkspaceTree } from "../workspaceTree"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mutator: Mutator
|
||||||
|
pageController: IPageController
|
||||||
|
workspaceTree: WorkspaceTree,
|
||||||
|
boardTree?: BoardTree
|
||||||
|
}
|
||||||
|
|
||||||
|
class Sidebar extends React.Component<Props> {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { workspaceTree } = this.props
|
||||||
|
const { boards } = workspaceTree
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="octo-sidebar">
|
||||||
|
{
|
||||||
|
boards.map(board => {
|
||||||
|
const displayTitle = board.title || "(Untitled Board)"
|
||||||
|
return (
|
||||||
|
<div key={board.id} className="octo-sidebar-item octo-hover-container">
|
||||||
|
<div className="octo-sidebar-title" onClick={() => { this.boardClicked(board) }}>{board.icon ? `${board.icon} ${displayTitle}` : displayTitle}</div>
|
||||||
|
<div className="octo-spacer"></div>
|
||||||
|
<div className="octo-button square octo-hover-item" onClick={(e) => { this.showOptions(e, board) }}><div className="imageOptions" /></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div className="octo-button" onClick={() => { this.addBoardClicked() }}>+ Add Board</div>
|
||||||
|
|
||||||
|
<div className="octo-spacer"></div>
|
||||||
|
|
||||||
|
<div className="octo-button" onClick={(e) => { this.settingsClicked(e) }}>Settings</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private showOptions(e: React.MouseEvent, board: Board) {
|
||||||
|
const { mutator, pageController, workspaceTree } = this.props
|
||||||
|
const { boards } = workspaceTree
|
||||||
|
|
||||||
|
const options: MenuOption[] = []
|
||||||
|
|
||||||
|
const nextBoardId = boards.length > 1 ? boards.find(o => o.id !== board.id).id : undefined
|
||||||
|
if (nextBoardId) {
|
||||||
|
options.push({ id: "delete", name: "Delete board" })
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu.shared.options = options
|
||||||
|
Menu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||||
|
switch (optionId) {
|
||||||
|
case "delete": {
|
||||||
|
mutator.deleteBlock(
|
||||||
|
board,
|
||||||
|
"delete block",
|
||||||
|
async () => { pageController.showBoard(nextBoardId!) },
|
||||||
|
async () => { pageController.showBoard(board.id) },
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Menu.shared.showAtElement(e.target as HTMLElement)
|
||||||
|
}
|
||||||
|
|
||||||
|
private settingsClicked(e: React.MouseEvent) {
|
||||||
|
const { mutator } = this.props
|
||||||
|
|
||||||
|
Menu.shared.options = [
|
||||||
|
{ id: "import", name: "Import Archive" },
|
||||||
|
{ id: "export", name: "Export Archive" },
|
||||||
|
]
|
||||||
|
Menu.shared.onMenuClicked = (optionId: string, type?: string) => {
|
||||||
|
switch (optionId) {
|
||||||
|
case "import": {
|
||||||
|
Archiver.importFullArchive(mutator, () => {
|
||||||
|
this.forceUpdate()
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case "export": {
|
||||||
|
Archiver.exportFullArchive(mutator)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HACKHACK: Show menu above (TODO: refactor menu code to do this automatically)
|
||||||
|
const element = e.target as HTMLElement
|
||||||
|
const bodyRect = document.body.getBoundingClientRect()
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
Menu.shared.showAt(rect.left - bodyRect.left + 20, rect.top - bodyRect.top - 30 * Menu.shared.options.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
private boardClicked(board: Board) {
|
||||||
|
const { pageController } = this.props
|
||||||
|
pageController.showBoard(board.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async addBoardClicked() {
|
||||||
|
const { mutator, boardTree, pageController } = this.props
|
||||||
|
|
||||||
|
const oldBoardId = boardTree?.board?.id
|
||||||
|
const board = new Board()
|
||||||
|
await mutator.insertBlock(
|
||||||
|
board,
|
||||||
|
"add board",
|
||||||
|
async () => { pageController.showBoard(board.id) },
|
||||||
|
async () => { if (oldBoardId) { pageController.showBoard(oldBoardId) } })
|
||||||
|
|
||||||
|
await mutator.insertBlock(board)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Sidebar }
|
|
@ -111,7 +111,7 @@ class TableComponent extends React.Component<Props, State> {
|
||||||
{/* Headers */}
|
{/* Headers */}
|
||||||
|
|
||||||
<div className="octo-table-header" id="mainBoardHeader">
|
<div className="octo-table-header" id="mainBoardHeader">
|
||||||
<div className="octo-table-cell" id="mainBoardHeader">
|
<div className="octo-table-cell title-cell" id="mainBoardHeader">
|
||||||
<div
|
<div
|
||||||
className="octo-label"
|
className="octo-label"
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
|
|
|
@ -36,7 +36,7 @@ class TableRow extends React.Component<Props, State> {
|
||||||
|
|
||||||
{/* Name / title */}
|
{/* Name / title */}
|
||||||
|
|
||||||
<div className="octo-table-cell" id="mainBoardHeader" onMouseOver={() => { openButonRef.current.style.display = null }} onMouseLeave={() => { openButonRef.current.style.display = "none" }}>
|
<div className="octo-table-cell title-cell" id="mainBoardHeader" onMouseOver={() => { openButonRef.current.style.display = null }} onMouseLeave={() => { openButonRef.current.style.display = "none" }}>
|
||||||
<div className="octo-icontitle">
|
<div className="octo-icontitle">
|
||||||
<div className="octo-icon">{card.icon}</div>
|
<div className="octo-icon">{card.icon}</div>
|
||||||
<Editable
|
<Editable
|
||||||
|
|
57
src/client/components/workspaceComponent.tsx
Normal file
57
src/client/components/workspaceComponent.tsx
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import React from "react"
|
||||||
|
import { BoardTree } from "../boardTree"
|
||||||
|
import { Mutator } from "../mutator"
|
||||||
|
import { IPageController } from "../octoTypes"
|
||||||
|
import { Utils } from "../utils"
|
||||||
|
import { WorkspaceTree } from "../workspaceTree"
|
||||||
|
import { BoardComponent } from "./boardComponent"
|
||||||
|
import { Sidebar } from "./sidebar"
|
||||||
|
import { TableComponent } from "./tableComponent"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
mutator: Mutator,
|
||||||
|
workspaceTree: WorkspaceTree
|
||||||
|
boardTree?: BoardTree
|
||||||
|
pageController: IPageController
|
||||||
|
}
|
||||||
|
|
||||||
|
class WorkspaceComponent extends React.Component<Props> {
|
||||||
|
render() {
|
||||||
|
const { mutator, boardTree, workspaceTree, pageController } = this.props
|
||||||
|
|
||||||
|
const element =
|
||||||
|
<div className="octo-workspace">
|
||||||
|
<Sidebar mutator={mutator} pageController={pageController} workspaceTree={workspaceTree} boardTree={boardTree}></Sidebar>
|
||||||
|
{this.mainComponent()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
private mainComponent() {
|
||||||
|
const { mutator, boardTree, pageController } = this.props
|
||||||
|
const { activeView } = boardTree || {}
|
||||||
|
|
||||||
|
if (!activeView) {
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (activeView?.viewType) {
|
||||||
|
case "board": {
|
||||||
|
return <BoardComponent mutator={mutator} boardTree={boardTree} pageController={pageController} />
|
||||||
|
}
|
||||||
|
|
||||||
|
case "table": {
|
||||||
|
return <TableComponent mutator={mutator} boardTree={boardTree} pageController={pageController} />
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
Utils.assertFailure(`render() Unhandled viewType: ${activeView.viewType}`)
|
||||||
|
return <div></div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkspaceComponent }
|
|
@ -50,7 +50,7 @@ class Mutator {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteBlock(block: IBlock, description?: string) {
|
async deleteBlock(block: IBlock, description?: string, beforeRedo?: () => Promise<void>, afterUndo?: () => Promise<void>) {
|
||||||
const { octo, undoManager } = this
|
const { octo, undoManager } = this
|
||||||
|
|
||||||
if (!description) {
|
if (!description) {
|
||||||
|
@ -59,10 +59,12 @@ class Mutator {
|
||||||
|
|
||||||
await undoManager.perform(
|
await undoManager.perform(
|
||||||
async () => {
|
async () => {
|
||||||
|
await beforeRedo?.()
|
||||||
await octo.deleteBlock(block.id)
|
await octo.deleteBlock(block.id)
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
await octo.insertBlock(block)
|
await octo.insertBlock(block)
|
||||||
|
await afterUndo?.()
|
||||||
},
|
},
|
||||||
description
|
description
|
||||||
)
|
)
|
||||||
|
|
|
@ -11,10 +11,10 @@ interface IBlock {
|
||||||
|
|
||||||
type: string
|
type: string
|
||||||
title?: string
|
title?: string
|
||||||
url?: string
|
url?: string // TODO: Move to properties (_url)
|
||||||
icon?: string
|
icon?: string
|
||||||
order: number
|
order: number
|
||||||
properties: IProperty[]
|
properties: IProperty[] // TODO: Change to map
|
||||||
|
|
||||||
createAt: number
|
createAt: number
|
||||||
updateAt: number
|
updateAt: number
|
||||||
|
@ -24,6 +24,7 @@ interface IBlock {
|
||||||
// These are methods exposed by the top-level page to components
|
// These are methods exposed by the top-level page to components
|
||||||
interface IPageController {
|
interface IPageController {
|
||||||
showCard(card: IBlock): Promise<void>
|
showCard(card: IBlock): Promise<void>
|
||||||
|
showBoard(boardId: string): void
|
||||||
showView(viewId: string): void
|
showView(viewId: string): void
|
||||||
showFilter(anchorElement?: HTMLElement): void
|
showFilter(anchorElement?: HTMLElement): void
|
||||||
setSearchText(text?: string): void
|
setSearchText(text?: string): void
|
||||||
|
|
23
src/client/workspaceTree.ts
Normal file
23
src/client/workspaceTree.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Board } from "./board"
|
||||||
|
import { OctoClient } from "./octoClient"
|
||||||
|
import { IBlock } from "./octoTypes"
|
||||||
|
|
||||||
|
class WorkspaceTree {
|
||||||
|
boards: Board[] = []
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private octo: OctoClient) {
|
||||||
|
}
|
||||||
|
|
||||||
|
async sync() {
|
||||||
|
const blocks = await this.octo.getBlocks(undefined, "board")
|
||||||
|
this.rebuild(blocks)
|
||||||
|
}
|
||||||
|
|
||||||
|
private rebuild(blocks: IBlock[]) {
|
||||||
|
const boardBlocks = blocks.filter(block => block.type === "board")
|
||||||
|
this.boards = boardBlocks.map(o => new Board(o))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { WorkspaceTree }
|
|
@ -5,6 +5,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
color: rgb(55, 53, 47);
|
color: rgb(55, 53, 47);
|
||||||
}
|
}
|
||||||
|
@ -60,19 +64,88 @@ hr {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* OCTO */
|
/* App frame */
|
||||||
|
|
||||||
|
#octo-tasks-app {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#octo-tasks-app > #main {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
|
||||||
|
.octo-workspace {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.octo-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
min-height: 100%;
|
||||||
|
background-color: rgb(247, 246, 243);
|
||||||
|
min-width: 230px;
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.octo-sidebar-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 3px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.octo-sidebar-title {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.octo-sidebar-item:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.octo-sidebar-item .octo-button:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main app */
|
||||||
|
|
||||||
|
.octo-app {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.octo-frame {
|
.octo-frame {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: scroll;
|
||||||
|
|
||||||
padding: 10px 95px 50px 95px;
|
padding: 10px 95px 50px 95px;
|
||||||
min-width: 1000px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-board {
|
.octo-board {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-controls {
|
.octo-controls {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
@ -115,12 +188,14 @@ hr {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-board-column {
|
.octo-board-column {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
width: 260px;
|
width: 260px;
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
|
@ -136,6 +211,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-board-card {
|
.octo-board-card {
|
||||||
|
flex: 0 0 auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -370,6 +446,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-icontitle {
|
.octo-icontitle {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -382,6 +459,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-board-card > .octo-icontitle {
|
.octo-board-card > .octo-icontitle {
|
||||||
|
flex: 1 1 auto;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -548,6 +626,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
.octo-table-cell {
|
.octo-table-cell {
|
||||||
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
|
@ -556,7 +635,7 @@ hr {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 5px 8px 6px 8px;
|
padding: 5px 8px 6px 8px;
|
||||||
|
|
||||||
width: 240px;
|
width: 150px;
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
@ -566,6 +645,10 @@ hr {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.octo-table-cell.title-cell {
|
||||||
|
width: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
.octo-table-cell .octo-propertyvalue {
|
.octo-table-cell .octo-propertyvalue {
|
||||||
line-height: 17px;
|
line-height: 17px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack")
|
||||||
const path = require("path");
|
const path = require("path")
|
||||||
const CopyPlugin = require("copy-webpack-plugin");
|
const CopyPlugin = require("copy-webpack-plugin")
|
||||||
var HtmlWebpackPlugin = require('html-webpack-plugin');
|
var HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||||
|
|
||||||
const outpath = path.resolve(__dirname, "pack");
|
const outpath = path.resolve(__dirname, "pack")
|
||||||
|
|
||||||
function makeCommonConfig() {
|
function makeCommonConfig() {
|
||||||
const commonConfig = {
|
const commonConfig = {
|
||||||
|
@ -47,20 +47,6 @@ function makeCommonConfig() {
|
||||||
{ from: path.resolve(__dirname, "node_modules/easymde/dist/easymde.min.css"), to: "static" },
|
{ from: path.resolve(__dirname, "node_modules/easymde/dist/easymde.min.css"), to: "static" },
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: true,
|
|
||||||
title: "OCTO",
|
|
||||||
chunks: [],
|
|
||||||
template: "html-templates/index.ejs",
|
|
||||||
filename: 'index.html'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: true,
|
|
||||||
title: "OCTO - Boards",
|
|
||||||
chunks: ["boardsPage"],
|
|
||||||
template: "html-templates/page.ejs",
|
|
||||||
filename: 'boards.html'
|
|
||||||
}),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
inject: true,
|
inject: true,
|
||||||
title: "OCTO",
|
title: "OCTO",
|
||||||
|
@ -70,16 +56,15 @@ function makeCommonConfig() {
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
entry: {
|
entry: {
|
||||||
boardsPage: "./src/client/boardsPage.ts",
|
|
||||||
boardPage: "./src/client/boardPage.tsx"
|
boardPage: "./src/client/boardPage.tsx"
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
path: outpath
|
path: outpath
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
return commonConfig;
|
return commonConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = makeCommonConfig;
|
module.exports = makeCommonConfig
|
||||||
|
|
Loading…
Reference in a new issue