focalboard/import/nextcloud-deck/importDeck.ts
2022-03-31 22:36:02 -04:00

222 lines
7.3 KiB
TypeScript

// 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<number> {
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<number, string>()
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<number, string>()
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 <nextcloud-url>] [-u <username>] [-p <password>] [-o <output-path>]')
exit(1)
}
main()