diff --git a/webapp/src/pages/boardPage.tsx b/webapp/src/pages/boardPage.tsx index 28fe69e84..6d5796318 100644 --- a/webapp/src/pages/boardPage.tsx +++ b/webapp/src/pages/boardPage.tsx @@ -3,7 +3,6 @@ import React from 'react' import {IBlock} from '../blocks/block' -import {MutableBoard} from '../blocks/board' import {sendFlashMessage} from '../components/flashMessages' import {WorkspaceComponent} from '../components/workspaceComponent' import mutator from '../mutator' @@ -159,8 +158,7 @@ export default class BoardPage extends React.Component { private async sync(boardId: string = this.state.boardId, viewId: string | undefined = this.state.viewId) { Utils.log(`sync start: ${boardId}`) - const workspaceTree = new MutableWorkspaceTree() - await workspaceTree.sync() + const workspaceTree = await MutableWorkspaceTree.sync() const boardIds = [...workspaceTree.boards.map((o) => o.id), ...workspaceTree.boardTemplates.map((o) => o.id)] this.setState({workspaceTree}) @@ -178,12 +176,9 @@ export default class BoardPage extends React.Component { ) if (boardId) { - const boardTree = await MutableBoardTree.sync(boardId) + const boardTree = await MutableBoardTree.sync(boardId, viewId) if (boardTree && boardTree.board) { - // Default to first view - boardTree.setActiveView(viewId || boardTree.views[0].id) - // Update url with viewId if it's different if (boardTree.activeView.id !== this.state.viewId) { Utils.replaceUrlQueryParam('v', boardTree.activeView.id) @@ -213,21 +208,20 @@ export default class BoardPage extends React.Component { let newState = {workspaceTree, boardTree, viewId} - const newWorkspaceTree = workspaceTree.mutableCopy() - if (newWorkspaceTree.incrementalUpdate(blocks)) { + const newWorkspaceTree = MutableWorkspaceTree.incrementalUpdate(workspaceTree, blocks) + if (newWorkspaceTree) { newState = {...newState, workspaceTree: newWorkspaceTree} } - let newBoardTree: MutableBoardTree | undefined + let newBoardTree: BoardTree | undefined if (boardTree) { newBoardTree = MutableBoardTree.incrementalUpdate(boardTree, blocks) } else if (this.state.boardId) { // Corner case: When the page is viewing a deleted board, that is subsequently un-deleted on another client - newBoardTree = await MutableBoardTree.sync(this.state.boardId) + newBoardTree = await MutableBoardTree.sync(this.state.boardId, this.state.viewId) } if (newBoardTree) { - newBoardTree.setActiveView(this.state.viewId) newState = {...newState, boardTree: newBoardTree, viewId: newBoardTree.activeView.id} } else { newState = {...newState, boardTree: undefined} @@ -260,8 +254,7 @@ export default class BoardPage extends React.Component { showView(viewId: string, boardId: string = this.state.boardId): void { if (this.state.boardTree && this.state.boardId === boardId) { - const newBoardTree = this.state.boardTree.mutableCopy() - newBoardTree.setActiveView(viewId) + const newBoardTree = this.state.boardTree.copyWithView(viewId) this.setState({boardTree: newBoardTree, viewId}) } else { this.attachToBoard(boardId, viewId) @@ -277,8 +270,7 @@ export default class BoardPage extends React.Component { return } - const newBoardTree = this.state.boardTree.mutableCopy() - newBoardTree.setSearchText(text) + const newBoardTree = this.state.boardTree.copyWithSearchText(text) this.setState({boardTree: newBoardTree}) } } diff --git a/webapp/src/viewModel/boardTree.test.ts b/webapp/src/viewModel/boardTree.test.ts index 37c1c316d..d9e668383 100644 --- a/webapp/src/viewModel/boardTree.test.ts +++ b/webapp/src/viewModel/boardTree.test.ts @@ -30,7 +30,7 @@ test('BoardTree', async () => { // Sync FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, view, view2, card, cardTemplate]))) - let boardTree = await MutableBoardTree.sync(board.id) + let boardTree = await MutableBoardTree.sync(board.id, view.id) expect(boardTree).not.toBeUndefined() if (!boardTree) { fail('sync') @@ -43,17 +43,16 @@ test('BoardTree', async () => { expect(boardTree.allBlocks).toEqual([board, view, view2, card, cardTemplate]) // Group / filter with sort - boardTree.setActiveView(view.id) expect(boardTree.activeView).toEqual(view) expect(boardTree.cards).toEqual([card]) // Group / filter without sort - boardTree.setActiveView(view2.id) + boardTree = boardTree.copyWithView(view2.id) expect(boardTree.activeView).toEqual(view2) expect(boardTree.cards).toEqual([card]) // Invalid view, defaults to first view - boardTree.setActiveView('invalid id') + boardTree = boardTree.copyWithView('invalid id') expect(boardTree.activeView).toEqual(view) // Incremental update @@ -62,7 +61,9 @@ test('BoardTree', async () => { const cardTemplate2 = TestBlockFactory.createCard(board) cardTemplate2.isTemplate = true + let originalBoardTree = boardTree boardTree = MutableBoardTree.incrementalUpdate(boardTree, [view3, card2, cardTemplate2]) + expect(boardTree).not.toBe(originalBoardTree) expect(boardTree).not.toBeUndefined() if (!boardTree) { fail('incrementalUpdate') @@ -72,36 +73,42 @@ test('BoardTree', async () => { expect(boardTree.cardTemplates).toEqual([cardTemplate, cardTemplate2]) // Group / filter with sort - boardTree.setActiveView(view.id) + originalBoardTree = boardTree + boardTree = boardTree.copyWithView(view.id) + expect(boardTree).not.toBe(originalBoardTree) expect(boardTree.activeView).toEqual(view) expect(boardTree.cards).toEqual([card, card2]) // Group / filter without sort - boardTree.setActiveView(view2.id) + originalBoardTree = boardTree + boardTree = boardTree.copyWithView(view2.id) + expect(boardTree).not.toBe(originalBoardTree) expect(boardTree.activeView).toEqual(view2) expect(boardTree.cards).toEqual([card, card2]) // Incremental update: No change const anotherBoard = TestBlockFactory.createBoard() const card4 = TestBlockFactory.createCard(anotherBoard) + originalBoardTree = boardTree boardTree = MutableBoardTree.incrementalUpdate(boardTree, [anotherBoard, card4]) + expect(boardTree).toBe(originalBoardTree) // Expect same value on no change expect(boardTree).not.toBeUndefined() if (!boardTree) { fail('incrementalUpdate') } // Copy - const boardTree2 = boardTree.mutableCopy() - expect(boardTree2.board).toEqual(boardTree.board) - expect(boardTree2.views).toEqual(boardTree.views) - expect(boardTree2.allCards).toEqual(boardTree.allCards) - expect(boardTree2.cardTemplates).toEqual(boardTree.cardTemplates) - expect(boardTree2.allBlocks).toEqual(boardTree.allBlocks) + // const boardTree2 = boardTree.mutableCopy() + // expect(boardTree2.board).toEqual(boardTree.board) + // expect(boardTree2.views).toEqual(boardTree.views) + // expect(boardTree2.allCards).toEqual(boardTree.allCards) + // expect(boardTree2.cardTemplates).toEqual(boardTree.cardTemplates) + // expect(boardTree2.allBlocks).toEqual(boardTree.allBlocks) // Search text const searchText = 'search text' expect(boardTree.getSearchText()).toBeUndefined() - boardTree.setSearchText(searchText) + boardTree = boardTree.copyWithSearchText(searchText) expect(boardTree.getSearchText()).toBe(searchText) }) @@ -111,7 +118,7 @@ test('BoardTree: defaults', async () => { // Sync FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board]))) - const boardTree = await MutableBoardTree.sync(board.id) + const boardTree = await MutableBoardTree.sync(board.id, 'noView') expect(boardTree).not.toBeUndefined() if (!boardTree) { fail('sync') @@ -125,6 +132,6 @@ test('BoardTree: defaults', async () => { expect(boardTree.cardTemplates).toEqual([]) // Match everything except for cardProperties - board.cardProperties = boardTree.board!.cardProperties + board.cardProperties = boardTree.board!.cardProperties.slice() expect(boardTree.board).toEqual(board) }) diff --git a/webapp/src/viewModel/boardTree.ts b/webapp/src/viewModel/boardTree.ts index be8efd599..d790095f7 100644 --- a/webapp/src/viewModel/boardTree.ts +++ b/webapp/src/viewModel/boardTree.ts @@ -32,7 +32,8 @@ interface BoardTree { getSearchText(): string | undefined orderedCards(): Card[] - mutableCopy(): MutableBoardTree + copyWithView(viewId: string): BoardTree + copyWithSearchText(searchText?: string): BoardTree } class MutableBoardTree implements BoardTree { @@ -58,22 +59,31 @@ class MutableBoardTree implements BoardTree { } // Factory methods - static async sync(boardId: string): Promise { + + static async sync(boardId: string, viewId: string): Promise { const rawBlocks = await octoClient.getSubtree(boardId) - return this.buildTree(boardId, rawBlocks) + const newBoardTree = this.buildTree(boardId, rawBlocks) + if (newBoardTree) { + newBoardTree.setActiveView(viewId) + } + return newBoardTree } - static incrementalUpdate(boardTree: BoardTree, updatedBlocks: IBlock[]): MutableBoardTree | undefined { + static incrementalUpdate(boardTree: BoardTree, updatedBlocks: IBlock[]): BoardTree | undefined { const relevantBlocks = updatedBlocks.filter((block) => block.deleteAt !== 0 || block.id === boardTree.board.id || block.parentId === boardTree.board.id) if (relevantBlocks.length < 1) { // No change - return boardTree.mutableCopy() + return boardTree } const rawBlocks = OctoUtils.mergeBlocks(boardTree.allBlocks, relevantBlocks) - return this.buildTree(boardTree.board.id, rawBlocks) + const newBoardTree = this.buildTree(boardTree.board.id, rawBlocks) + if (newBoardTree && boardTree.activeView) { + newBoardTree.setActiveView(boardTree.activeView.id) + } + return newBoardTree } - static buildTree(boardId: string, sourceBlocks: readonly IBlock[]): MutableBoardTree | undefined { + private static buildTree(boardId: string, sourceBlocks: readonly IBlock[]): MutableBoardTree | undefined { const blocks = OctoUtils.hydrateBlocks(sourceBlocks) const board = blocks.find((block) => block.type === 'board' && block.id === boardId) as MutableBoard if (!board) { @@ -127,9 +137,9 @@ class MutableBoardTree implements BoardTree { return didChange } - setActiveView(viewId: string): void { + private setActiveView(viewId: string): void { let view = this.views.find((o) => o.id === viewId) - if (!view) { + if (!view || !viewId) { Utils.logError(`Cannot find BoardView: ${viewId}`) view = this.views[0] } @@ -148,7 +158,7 @@ class MutableBoardTree implements BoardTree { return this.searchText } - setSearchText(text?: string): void { + private setSearchText(text?: string): void { this.searchText = text this.applyFilterSortAndGroup() } @@ -412,9 +422,21 @@ class MutableBoardTree implements BoardTree { return cards } - mutableCopy(): MutableBoardTree { + private mutableCopy(): MutableBoardTree { return MutableBoardTree.buildTree(this.board.id, this.allBlocks)! } + + copyWithView(viewId: string): BoardTree { + const boardTree = this.mutableCopy() + boardTree.setActiveView(viewId) + return boardTree + } + + copyWithSearchText(searchText?: string): BoardTree { + const boardTree = this.mutableCopy() + boardTree.setSearchText(searchText) + return boardTree + } } export {MutableBoardTree, BoardTree, Group as BoardTreeGroup} diff --git a/webapp/src/viewModel/cardTree.test.ts b/webapp/src/viewModel/cardTree.test.ts index 9eb5bce60..96a892335 100644 --- a/webapp/src/viewModel/cardTree.test.ts +++ b/webapp/src/viewModel/cardTree.test.ts @@ -8,7 +8,7 @@ import 'isomorphic-fetch' import {TestBlockFactory} from '../test/testBlockFactory' import {FetchMock} from '../test/fetchMock' -import {MutableCardTree} from './cardTree' +import {CardTree, MutableCardTree} from './cardTree' global.fetch = FetchMock.fn @@ -25,7 +25,7 @@ test('CardTree', async () => { const divider = TestBlockFactory.createDivider(card) FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([card, comment, text, image, divider]))) - let cardTree = await MutableCardTree.sync(card.id) + let cardTree: CardTree | undefined = await MutableCardTree.sync(card.id) expect(cardTree).not.toBeUndefined() if (!cardTree) { fail('sync') @@ -53,15 +53,17 @@ test('CardTree', async () => { // Incremental update: No change const anotherCard = TestBlockFactory.createCard() const comment3 = TestBlockFactory.createComment(anotherCard) + const originalCardTree = cardTree cardTree = MutableCardTree.incrementalUpdate(cardTree, [comment3]) + expect(cardTree).toBe(originalCardTree) expect(cardTree).not.toBeUndefined() if (!cardTree) { fail('incrementalUpdate') } // Copy - const cardTree2 = cardTree.mutableCopy() - expect(cardTree2.card).toEqual(cardTree.card) - expect(cardTree2.comments).toEqual(cardTree.comments) - expect(cardTree2.contents).toEqual(cardTree.contents) + // const cardTree2 = cardTree.mutableCopy() + // expect(cardTree2.card).toEqual(cardTree.card) + // expect(cardTree2.comments).toEqual(cardTree.comments) + // expect(cardTree2.contents).toEqual(cardTree.contents) }) diff --git a/webapp/src/viewModel/cardTree.ts b/webapp/src/viewModel/cardTree.ts index 072a0ca0a..725ac1958 100644 --- a/webapp/src/viewModel/cardTree.ts +++ b/webapp/src/viewModel/cardTree.ts @@ -11,8 +11,6 @@ interface CardTree { readonly comments: readonly IBlock[] readonly contents: readonly IOrderedBlock[] readonly allBlocks: readonly IBlock[] - - mutableCopy(): MutableCardTree } class MutableCardTree implements CardTree { @@ -30,22 +28,22 @@ class MutableCardTree implements CardTree { // Factory methods - static async sync(boardId: string): Promise { + static async sync(boardId: string): Promise { const rawBlocks = await octoClient.getSubtree(boardId) return this.buildTree(boardId, rawBlocks) } - static incrementalUpdate(cardTree: CardTree, updatedBlocks: IBlock[]): MutableCardTree | undefined { + static incrementalUpdate(cardTree: CardTree, updatedBlocks: IBlock[]): CardTree | undefined { const relevantBlocks = updatedBlocks.filter((block) => block.deleteAt !== 0 || block.id === cardTree.card.id || block.parentId === cardTree.card.id) if (relevantBlocks.length < 1) { // No change - return cardTree.mutableCopy() + return cardTree } const rawBlocks = OctoUtils.mergeBlocks(cardTree.allBlocks, relevantBlocks) return this.buildTree(cardTree.card.id, rawBlocks) } - static buildTree(cardId: string, sourceBlocks: readonly IBlock[]): MutableCardTree | undefined { + private static buildTree(cardId: string, sourceBlocks: readonly IBlock[]): MutableCardTree | undefined { const blocks = OctoUtils.hydrateBlocks(sourceBlocks) const card = blocks.find((o) => o.type === 'card' && o.id === cardId) as MutableCard @@ -63,7 +61,7 @@ class MutableCardTree implements CardTree { return cardTree } - mutableCopy(): MutableCardTree { + private mutableCopy(): MutableCardTree { return MutableCardTree.buildTree(this.card.id, this.allBlocks)! } } diff --git a/webapp/src/viewModel/workspaceTree.test.ts b/webapp/src/viewModel/workspaceTree.test.ts index 63f30f0cf..4e1bd740b 100644 --- a/webapp/src/viewModel/workspaceTree.test.ts +++ b/webapp/src/viewModel/workspaceTree.test.ts @@ -23,8 +23,11 @@ test('WorkspaceTree', async () => { // Sync FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([board, boardTemplate]))) FetchMock.fn.mockReturnValueOnce(FetchMock.jsonResponse(JSON.stringify([view]))) - const workspaceTree = new MutableWorkspaceTree() - await workspaceTree.sync() + let workspaceTree = await MutableWorkspaceTree.sync() + expect(workspaceTree).not.toBeUndefined() + if (!workspaceTree) { + fail('sync') + } expect(FetchMock.fn).toBeCalledTimes(2) expect(workspaceTree.boards).toEqual([board]) @@ -37,21 +40,31 @@ test('WorkspaceTree', async () => { boardTemplate2.isTemplate = true const view2 = TestBlockFactory.createBoardView(board2) - expect(workspaceTree.incrementalUpdate([board2, boardTemplate2, view2])).toBe(true) + workspaceTree = MutableWorkspaceTree.incrementalUpdate(workspaceTree, [board2, boardTemplate2, view2]) + expect(workspaceTree).not.toBeUndefined() + if (!workspaceTree) { + fail('incrementalUpdate') + } expect(workspaceTree.boards).toEqual([board, board2]) expect(workspaceTree.boardTemplates).toEqual([boardTemplate, boardTemplate2]) expect(workspaceTree.views).toEqual([view, view2]) // Incremental update: No change const card = TestBlockFactory.createCard() - expect(workspaceTree.incrementalUpdate([card])).toBe(false) + const originalWorkspaceTree = workspaceTree + workspaceTree = MutableWorkspaceTree.incrementalUpdate(workspaceTree, [card]) + expect(workspaceTree).toBe(originalWorkspaceTree) + expect(workspaceTree).not.toBeUndefined() + if (!workspaceTree) { + fail('incrementalUpdate') + } expect(workspaceTree.boards).toEqual([board, board2]) expect(workspaceTree.boardTemplates).toEqual([boardTemplate, boardTemplate2]) expect(workspaceTree.views).toEqual([view, view2]) // Copy - const workspaceTree2 = workspaceTree.mutableCopy() - expect(workspaceTree2.boards).toEqual(workspaceTree.boards) - expect(workspaceTree2.boardTemplates).toEqual(workspaceTree.boardTemplates) - expect(workspaceTree2.views).toEqual(workspaceTree.views) + // const workspaceTree2 = workspaceTree.mutableCopy() + // expect(workspaceTree2.boards).toEqual(workspaceTree.boards) + // expect(workspaceTree2.boardTemplates).toEqual(workspaceTree.boardTemplates) + // expect(workspaceTree2.views).toEqual(workspaceTree.views) }) diff --git a/webapp/src/viewModel/workspaceTree.ts b/webapp/src/viewModel/workspaceTree.ts index f62aeca81..a6d3608c0 100644 --- a/webapp/src/viewModel/workspaceTree.ts +++ b/webapp/src/viewModel/workspaceTree.ts @@ -10,49 +10,54 @@ interface WorkspaceTree { readonly boards: readonly Board[] readonly boardTemplates: readonly Board[] readonly views: readonly BoardView[] - - mutableCopy(): MutableWorkspaceTree + readonly allBlocks: readonly IBlock[] } class MutableWorkspaceTree { boards: Board[] = [] boardTemplates: Board[] = [] views: BoardView[] = [] + get allBlocks(): IBlock[] { + return [...this.boards, ...this.boardTemplates, ...this.views] + } - private rawBlocks: IBlock[] = [] + // Factory methods - async sync(): Promise { + static async sync(): Promise { const rawBoards = await octoClient.getBlocksWithType('board') const rawViews = await octoClient.getBlocksWithType('view') - this.rawBlocks = [...rawBoards, ...rawViews] - this.rebuild(OctoUtils.hydrateBlocks(this.rawBlocks)) + const rawBlocks = [...rawBoards, ...rawViews] + return this.buildTree(rawBlocks) } - incrementalUpdate(updatedBlocks: IBlock[]): boolean { + static incrementalUpdate(workspaceTree: WorkspaceTree, updatedBlocks: IBlock[]): WorkspaceTree { const relevantBlocks = updatedBlocks.filter((block) => block.deleteAt !== 0 || block.type === 'board' || block.type === 'view') if (relevantBlocks.length < 1) { - return false + // No change + return workspaceTree } - this.rawBlocks = OctoUtils.mergeBlocks(this.rawBlocks, updatedBlocks) - this.rebuild(OctoUtils.hydrateBlocks(this.rawBlocks)) - return true + const rawBlocks = OctoUtils.mergeBlocks(workspaceTree.allBlocks, relevantBlocks) + return this.buildTree(rawBlocks) } - private rebuild(blocks: IBlock[]) { - const allBoards = blocks.filter((block) => block.type === 'board') as Board[] - this.boards = allBoards.filter((block) => !block.isTemplate). - sort((a, b) => a.title.localeCompare(b.title)) as Board[] - this.boardTemplates = allBoards.filter((block) => block.isTemplate). - sort((a, b) => a.title.localeCompare(b.title)) as Board[] - this.views = blocks.filter((block) => block.type === 'view'). - sort((a, b) => a.title.localeCompare(b.title)) as BoardView[] - } + private static buildTree(sourceBlocks: readonly IBlock[]): MutableWorkspaceTree { + const blocks = OctoUtils.hydrateBlocks(sourceBlocks) - mutableCopy(): MutableWorkspaceTree { const workspaceTree = new MutableWorkspaceTree() - workspaceTree.incrementalUpdate(this.rawBlocks) + const allBoards = blocks.filter((block) => block.type === 'board') as Board[] + workspaceTree.boards = allBoards.filter((block) => !block.isTemplate). + sort((a, b) => a.title.localeCompare(b.title)) as Board[] + workspaceTree.boardTemplates = allBoards.filter((block) => block.isTemplate). + sort((a, b) => a.title.localeCompare(b.title)) as Board[] + workspaceTree.views = blocks.filter((block) => block.type === 'view'). + sort((a, b) => a.title.localeCompare(b.title)) as BoardView[] + return workspaceTree } + + private mutableCopy(): MutableWorkspaceTree { + return MutableWorkspaceTree.buildTree(this.allBlocks)! + } } export {MutableWorkspaceTree, WorkspaceTree}