// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import {MutableBlock} from './blocks/block' import {Board, IPropertyOption, IPropertyTemplate, MutableBoard, PropertyType} from './blocks/board' import {BoardView, ISortOption, MutableBoardView} from './blocks/boardView' import {Card, MutableCard} from './blocks/card' import {MutableImageBlock} from './blocks/imageBlock' import {IOrderedBlock, MutableOrderedBlock} from './blocks/orderedBlock' import {BoardTree} from './boardTree' import {FilterGroup} from './filterGroup' import octoClient from './octoClient' import {IBlock} from './octoTypes' import undoManager from './undomanager' import {Utils} from './utils' // // The Mutator is used to make all changes to server state // It also ensures that the Undo-manager is called for each action // class Mutator { async insertBlock(block: IBlock, description = 'add', afterRedo?: () => Promise, beforeUndo?: () => Promise) { await undoManager.perform( async () => { await octoClient.insertBlock(block) await afterRedo?.() }, async () => { await beforeUndo?.() await octoClient.deleteBlock(block.id) }, description, ) } async insertBlocks(blocks: IBlock[], description = 'add', afterRedo?: () => Promise, beforeUndo?: () => Promise) { await undoManager.perform( async () => { await octoClient.insertBlocks(blocks) await afterRedo?.() }, async () => { await beforeUndo?.() for (const block of blocks) { await octoClient.deleteBlock(block.id) } }, description, ) } async deleteBlock(block: IBlock, description?: string, beforeRedo?: () => Promise, afterUndo?: () => Promise) { if (!description) { description = `delete ${block.type}` } await undoManager.perform( async () => { await beforeRedo?.() await octoClient.deleteBlock(block.id) }, async () => { await octoClient.insertBlock(block) await afterUndo?.() }, description, ) } async changeTitle(block: IBlock, title: string, description = 'change title') { const newBlock = new MutableBlock(block) newBlock.title = title await undoManager.perform( async () => { await octoClient.updateBlock(newBlock) }, async () => { await octoClient.updateBlock(block) }, description, ) } async changeIcon(block: Card | Board, icon: string, description = 'change icon') { var newBlock: IBlock switch (block.type) { case 'card': { const card = new MutableCard(block) card.icon = icon newBlock = card break } case 'board': { const board = new MutableBoard(block) board.icon = icon newBlock = board break } } await undoManager.perform( async () => { await octoClient.updateBlock(newBlock) }, async () => { await octoClient.updateBlock(block) }, description, ) } async changeOrder(block: IOrderedBlock, order: number, description = 'change order') { const newBlock = new MutableOrderedBlock(block) newBlock.order = order await undoManager.perform( async () => { await octoClient.updateBlock(newBlock) }, async () => { await octoClient.updateBlock(block) }, description, ) } // Property Templates async insertPropertyTemplate(boardTree: BoardTree, index = -1, template?: IPropertyTemplate) { const {board, activeView} = boardTree if (index < 0) { index = board.cardProperties.length } if (!template) { template = { id: Utils.createGuid(), name: 'New Property', type: 'text', options: [], } } const oldBlocks: IBlock[] = [board] const newBoard = new MutableBoard(board) newBoard.cardProperties.splice(index, 0, template) const changedBlocks: IBlock[] = [newBoard] let description = 'add property' if (activeView.viewType === 'table') { oldBlocks.push(activeView) const newActiveView = new MutableBoardView(activeView) newActiveView.visiblePropertyIds.push(template.id) changedBlocks.push(newActiveView) description = 'add column' } await undoManager.perform( async () => { await octoClient.updateBlocks(changedBlocks) }, async () => { await octoClient.updateBlocks(oldBlocks) }, description, ) } async duplicatePropertyTemplate(boardTree: BoardTree, propertyId: string): Promise { const {board, activeView} = boardTree const oldBlocks: IBlock[] = [board] const newBoard = new MutableBoard(board) const changedBlocks: IBlock[] = [newBoard] const index = newBoard.cardProperties.findIndex((o) => o.id === propertyId) if (index === -1) { Utils.assertFailure(`Cannot find template with id: ${propertyId}`) return } const srcTemplate = newBoard.cardProperties[index] const newTemplate: IPropertyTemplate = { id: Utils.createGuid(), name: `Copy of ${srcTemplate.name}`, type: srcTemplate.type, options: srcTemplate.options.slice(), } newBoard.cardProperties.splice(index + 1, 0, newTemplate) let description = 'duplicate property' if (activeView.viewType === 'table') { oldBlocks.push(activeView) const newActiveView = new MutableBoardView(activeView) newActiveView.visiblePropertyIds.push(newTemplate.id) changedBlocks.push(newActiveView) description = 'duplicate column' } await undoManager.perform( async () => { await octoClient.updateBlocks(changedBlocks) }, async () => { await octoClient.updateBlocks(oldBlocks) }, description, ) return changedBlocks } async changePropertyTemplateOrder(board: Board, template: IPropertyTemplate, destIndex: number) { const templates = board.cardProperties const newValue = templates.slice() const srcIndex = templates.indexOf(template) Utils.log(`srcIndex: ${srcIndex}, destIndex: ${destIndex}`) newValue.splice(destIndex, 0, newValue.splice(srcIndex, 1)[0]) const newBoard = new MutableBoard(board) newBoard.cardProperties = newValue await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'reorder properties', ) } async deleteProperty(boardTree: BoardTree, propertyId: string) { const {board, views, cards} = boardTree const oldBlocks: IBlock[] = [board] const newBoard = new MutableBoard(board) const changedBlocks: IBlock[] = [newBoard] newBoard.cardProperties = board.cardProperties.filter((o) => o.id !== propertyId) views.forEach((view) => { if (view.visiblePropertyIds.includes(propertyId)) { oldBlocks.push(view) const newView = new MutableBoardView(view) newView.visiblePropertyIds = view.visiblePropertyIds.filter((o) => o !== propertyId) changedBlocks.push(newView) } }) cards.forEach((card) => { if (card.properties[propertyId]) { oldBlocks.push(card) const newCard = new MutableCard(card) delete newCard.properties[propertyId] changedBlocks.push(newCard) } }) await undoManager.perform( async () => { await octoClient.updateBlocks(changedBlocks) }, async () => { await octoClient.updateBlocks(oldBlocks) }, 'delete property', ) } async renameProperty(board: Board, propertyId: string, name: string) { const newBoard = new MutableBoard(board) const template = newBoard.cardProperties.find((o) => o.id === propertyId) if (!template) { Utils.assertFailure(`Can't find property template with Id: ${propertyId}`) return } Utils.log(`renameProperty from ${template.name} to ${name}`) template.name = name await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'rename property', ) } // Properties async insertPropertyOption(boardTree: BoardTree, template: IPropertyTemplate, option: IPropertyOption, description = 'add option') { const {board} = boardTree Utils.assert(board.cardProperties.includes(template)) const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === template.id) newTemplate.options.push(option) await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { // TODO: Also remove property on cards await octoClient.updateBlock(board) }, description, ) } async deletePropertyOption(boardTree: BoardTree, template: IPropertyTemplate, option: IPropertyOption) { const {board} = boardTree const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === template.id) newTemplate.options = newTemplate.options.filter(o => o.value !== option.value) // TODO: Also remove property on cards await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'delete option', ) } async changePropertyOptionOrder(board: Board, template: IPropertyTemplate, option: IPropertyOption, destIndex: number) { const srcIndex = template.options.indexOf(option) Utils.log(`srcIndex: ${srcIndex}, destIndex: ${destIndex}`) const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === template.id) newTemplate.options.splice(destIndex, 0, newTemplate.options.splice(srcIndex, 1)[0]) await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'reorder options', ) } async changePropertyOptionValue(boardTree: BoardTree, propertyTemplate: IPropertyTemplate, option: IPropertyOption, value: string) { const {board, cards} = boardTree const oldValue = option.value const oldBlocks: IBlock[] = [board] const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === propertyTemplate.id) const newOption = newTemplate.options.find(o => o.value === oldValue) newOption.value = value const changedBlocks: IBlock[] = [newBoard] // Change the value on all cards that have this property too for (const card of cards) { const propertyValue = card.properties[propertyTemplate.id] if (propertyValue && propertyValue === oldValue) { oldBlocks.push(card) const newCard = new MutableCard(card) newCard.properties[propertyTemplate.id] = value changedBlocks.push(newCard) } } await undoManager.perform( async () => { await octoClient.updateBlocks(changedBlocks) }, async () => { await octoClient.updateBlocks(oldBlocks) }, 'rename option', ) return changedBlocks } async changePropertyOptionColor(board: Board, template: IPropertyTemplate, option: IPropertyOption, color: string) { const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === template.id) const newOption = newTemplate.options.find(o => o.value === option.value) newOption.color = color undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'change option color', ) } async changePropertyValue(card: Card, propertyId: string, value?: string, description = 'change property') { const newCard = new MutableCard(card) newCard.properties[propertyId] = value await undoManager.perform( async () => { await octoClient.updateBlock(newCard) }, async () => { await octoClient.updateBlock(card) }, description, ) } async changePropertyType(board: Board, propertyTemplate: IPropertyTemplate, type: PropertyType) { const newBoard = new MutableBoard(board) const newTemplate = newBoard.cardProperties.find(o => o.id === propertyTemplate.id) newTemplate.type = type await undoManager.perform( async () => { await octoClient.updateBlock(newBoard) }, async () => { await octoClient.updateBlock(board) }, 'change property type', ) } // Views async changeViewSortOptions(view: BoardView, sortOptions: ISortOption[]) { const newView = new MutableBoardView(view) newView.sortOptions = sortOptions await undoManager.perform( async () => { await octoClient.updateBlock(newView) }, async () => { await octoClient.updateBlock(view) }, 'sort', ) } async changeViewFilter(view: BoardView, filter?: FilterGroup) { const newView = new MutableBoardView(view) newView.filter = filter await undoManager.perform( async () => { await octoClient.updateBlock(newView) }, async () => { await octoClient.updateBlock(view) }, 'filter', ) } async changeViewVisibleProperties(view: BoardView, visiblePropertyIds: string[]) { const newView = new MutableBoardView(view) newView.visiblePropertyIds = visiblePropertyIds await undoManager.perform( async () => { await octoClient.updateBlock(newView) }, async () => { await octoClient.updateBlock(view) }, 'hide / show property', ) } async changeViewGroupById(view: BoardView, groupById: string) { const newView = new MutableBoardView(view) newView.groupById = groupById await undoManager.perform( async () => { await octoClient.updateBlock(newView) }, async () => { await octoClient.updateBlock(view) }, 'group by', ) } // Not a mutator, but convenient to put here since Mutator wraps OctoClient async exportFullArchive() { return octoClient.exportFullArchive() } // Not a mutator, but convenient to put here since Mutator wraps OctoClient async importFullArchive(blocks: IBlock[]) { return octoClient.importFullArchive(blocks) } async createImageBlock(parentId: string, file: File, order = 1000): Promise { const url = await octoClient.uploadFile(file) if (!url) { return undefined } const block = new MutableImageBlock() block.parentId = parentId block.order = order block.url = url await undoManager.perform( async () => { await octoClient.insertBlock(block) }, async () => { await octoClient.deleteBlock(block.id) }, 'group by', ) return block } async undo() { await undoManager.undo() } undoDescription(): string | undefined { return undoManager.undoDescription } async redo() { await undoManager.redo() } redoDescription(): string | undefined { return undoManager.redoDescription } } const mutator = new Mutator() export default mutator export {mutator}