Refactor ViewModel (*Tree) classes

This commit is contained in:
Chen-I Lim 2020-12-09 19:08:07 -08:00
parent 4b744bd395
commit 6faf3cef63
7 changed files with 124 additions and 85 deletions

View file

@ -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<Props, State> {
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<Props, State> {
)
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<Props, State> {
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<Props, State> {
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<Props, State> {
return
}
const newBoardTree = this.state.boardTree.mutableCopy()
newBoardTree.setSearchText(text)
const newBoardTree = this.state.boardTree.copyWithSearchText(text)
this.setState({boardTree: newBoardTree})
}
}

View file

@ -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)
})

View file

@ -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<MutableBoardTree | undefined> {
static async sync(boardId: string, viewId: string): Promise<BoardTree | undefined> {
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}

View file

@ -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)
})

View file

@ -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<MutableCardTree | undefined> {
static async sync(boardId: string): Promise<CardTree | undefined> {
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)!
}
}

View file

@ -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)
})

View file

@ -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<void> {
static async sync(): Promise<WorkspaceTree> {
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}