// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. import * as fs from 'fs' import minimist from 'minimist' import {exit} from 'process' import {ArchiveUtils} from '../util/archive' import {Block} from '../../webapp/src/blocks/block' import {Board as FBBoard} from '../../webapp/src/blocks/board' import {IPropertyOption, IPropertyTemplate, createBoard} from '../../webapp/src/blocks/board' import {createBoardView} from '../../webapp/src/blocks/boardView' import {createCard} from '../../webapp/src/blocks/card' import {createTextBlock} from '../../webapp/src/blocks/textBlock' import {NextcloudDeckClient, Stack, Board} from './deck' import {Utils} from './utils' import readline from 'readline-sync' import {createCommentBlock} from '../../webapp/src/blocks/commentBlock' // HACKHACK: To allow Utils.CreateGuid to work (global.window as any) = {} const optionColors = [ // 'propColorDefault', 'propColorGray', 'propColorBrown', 'propColorOrange', 'propColorYellow', 'propColorGreen', 'propColorBlue', 'propColorPurple', 'propColorPink', 'propColorRed', ] async function main() { const args: minimist.ParsedArgs = minimist(process.argv.slice(2)) console.log("Transform a nextcloud deck into a mattermost Board.") if (args['h'] || args['help']) { showHelp() } // Get Options const url = args['url'] ?? readline.question('Nextcloud URL: ') const username = args['u'] ?? readline.question('Username: ') const password = args['p'] ?? readline.question('Password: ', {hideEchoBack: true}) const boardIdString = args['b'] const outputFile = args['o'] || 'archive.focalboard' // Create Client const deckClient = new NextcloudDeckClient({auth: {username, password}, url}) // Select board (Either from cli or by interactive selection) const boardId = boardIdString ? parseInt(boardIdString) : await selectBoard(deckClient) // Get Data const board = await deckClient.getBoardDetails(boardId) const stacks = await Promise.all((await deckClient.getStacks(boardId)).map(async s => { return { ...s, cards: await Promise.all(s.cards.map(async c => { if (c.commentsCount > 0) { c.comments = await deckClient.getComments(c.id) } return c })) } })) // Convert const [boards, blocks] = convert(board, stacks) // // Save output const outputData = ArchiveUtils.buildBlockArchive(boards, blocks) fs.writeFileSync(outputFile, outputData) console.log(`Exported to ${outputFile}`) } async function selectBoard(deckClient: NextcloudDeckClient): Promise { console.log("\nAvailable boards for this user:") const boards = await deckClient.getBoards() boards.forEach(b => console.log(`\t${b.id}: ${b.title} (${b.owner.uid})`)) return readline.questionInt("Enter Board ID: ") } function convert(deckBoard: Board, stacks: Stack[]): [FBBoard[], Block[]] { const boards: FBBoard[] = [] const blocks: Block[] = [] // Board const board = createBoard() console.log(`Board: ${deckBoard.title}`) board.title = deckBoard.title let colorIndex = 0 // Convert stacks (columns) to a Select property const stackOptionsIdMap = new Map() const stackOptions: IPropertyOption[] = [] stacks.forEach(stack => { const optionId = Utils.createGuid() stackOptionsIdMap.set(stack.id, optionId) const color = optionColors[colorIndex % optionColors.length] colorIndex += 1 const option: IPropertyOption = { id: optionId, value: stack.title, color, } stackOptions.push(option) }) const stackProperty: IPropertyTemplate = { id: Utils.createGuid(), name: 'List', type: 'select', options: stackOptions } // Convert labels (tags) to a Select property const labelOptionsIdMap = new Map() const labelOptions: IPropertyOption[] = [] deckBoard.labels.forEach(label => { const optionId = Utils.createGuid() labelOptionsIdMap.set(label.id, optionId) const color = optionColors[colorIndex % optionColors.length] colorIndex += 1 const option: IPropertyOption = { id: optionId, value: label.title, color, } labelOptions.push(option) }) const labelProperty: IPropertyTemplate = { id: Utils.createGuid(), name: 'Label', type: 'multiSelect', options: labelOptions } const dueDateProperty: IPropertyTemplate = { id: Utils.createGuid(), name: 'Due Date', type: 'date', options: [] } board.cardProperties = [stackProperty, labelProperty, dueDateProperty] boards.push(board) // Board view const view = createBoardView() view.title = 'Board View' view.fields.viewType = 'board' view.boardId = board.id view.parentId = board.id blocks.push(view) // Cards stacks.forEach(stack => stack.cards.forEach( card => { console.log(`Card: ${card.title}`) const outCard = createCard() outCard.title = card.title outCard.boardId = board.id outCard.parentId = board.id // Map Stacks to Select property options const stackOptionId = stackOptionsIdMap.get(card.stackId) if (stackOptionId) { outCard.fields.properties[stackProperty.id] = stackOptionId } else { console.warn(`Invalid idList: ${card.stackId} for card: ${card.title}`) } // Map Labels to Multiselect options outCard.fields.properties[labelProperty.id] = card.labels?.map(label => labelOptionsIdMap.get(label.id)).filter((id): id is string => !!id) // Add duedate if (card.duedate) { const duedate = new Date(card.duedate) outCard.fields.properties[dueDateProperty.id] = `{\"from\":${duedate.getTime()}}` } blocks.push(outCard) // Description if (card.description) { const text = createTextBlock() text.title = card.description text.boardId = board.id text.parentId = outCard.id blocks.push(text) outCard.fields.contentOrder = [text.id] } // Add Comments (Author cannot be determined since uid's are different) card.comments?.forEach(comment => { const commentBlock = createCommentBlock() commentBlock.title = comment.message commentBlock.boardId = board.id commentBlock.parentId = outCard.id blocks.push(commentBlock) }) }) ) console.log('') console.log(`Transformed Board ${deckBoard.title} into ${blocks.length} blocks.`) return [boards, blocks] } function showHelp() { console.log('import [--url ] [-u ] [-p ] [-o ]') exit(1) } main()