Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[GH-450] Feature: Add import csv feature #4920

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions import/csv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
test
16 changes: 16 additions & 0 deletions import/csv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# CSV importer

This node app converts a CSV into a Focalboard archive. To use:
1. Run `npm install` from within `focalboard/webapp`
2. Run `npm install` from within `focalboard/import/csv`
3. Run `npx ts-node importCsv.ts -i <path to csv> -o archive.boardarchive`
- If the csv was exported by testrails, pass `-t true` into the command line arguments
4. In Focalboard, click `Settings`, then `Import archive` and select `archive.boardarchive`

## Import scope

Currently, the script imports all cards from a single board, including their properties and markdown content.

The script currently imports all card properties as a Select type. You can change the type after importing into Focalboard.

[Contribute code](https://mattermost.github.io/focalboard/) to expand this.
171 changes: 171 additions & 0 deletions import/csv/importCsv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import csv from 'csvtojson'
import * as fs from 'fs'
import minimist from 'minimist'
import path from 'path'
import {exit} from 'process'
import {ArchiveUtils} from '../util/archive'
import {Block} from '../../webapp/src/blocks/block'
import {Board} from '../../webapp/src/blocks/board'
import {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 {Utils} from './utils'

(global.window as any) = {}

const optionColors = [
'propColorGray',
'propColorBrown',
'propColorOrange',
'propColorYellow',
'propColorGreen',
'propColorBlue',
'propColorPurple',
'propColorPink',
'propColorRed',
]
let optionColorIndex = 0

async function main() {
const args: minimist.ParsedArgs = minimist(process.argv.slice(2))

const inputFile = args['i']
const outputFile = args['o'] || 'test/archive.boardarchive'
const testrailFormat = (args['t'] === 'true') || false

if (!inputFile) {
showHelp()
}

if (!fs.existsSync(inputFile)){
console.log(`File not found: ${inputFile}`)
exit(2)
}

console.log(`InputFile: ${inputFile}`)
const input = await csv().fromFile(inputFile)
console.log(`Read ${input.length} rows.`)
console.log(input)

const title = path.basename(inputFile, '.csv')
console.log(`Title: ${title}`)

const [boards, blocks] = convert(input, title, testrailFormat)
const outputData = ArchiveUtils.buildBlockArchive(boards, blocks)

fs.writeFileSync(outputFile, outputData)
console.log(`Exported to ${outputFile}`)
}

function convert(input: any[], title: string, testrailFormat: boolean): [Board[], Block[]] {
const boards: Board[] = []
const blocks: Block[] = []

// Board
const board = createBoard()
console.log(`Board: ${title}`)
board.title = title

// Each column is a card property
const columns = getColumns(input)
columns.forEach(column => {
if(column === "Steps" && testrailFormat) {
return
} else {
const cardProperty: IPropertyTemplate = {
id: Utils.createGuid(),
name: column,
type: 'select',
options: []
}
board.cardProperties.push(cardProperty)
}
})

// Set all column types to select
// TODO: Detect column type
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
input.forEach(row => {
const keys = Object.keys(row)
console.log(keys)
if (keys.length < 1) {
console.error(`Expected at least one column`)
return blocks
}
const titleKey = keys[0]
const title = row[titleKey]

console.log(`Card: ${title}`)

const outCard = createCard()
outCard.title = title
outCard.boardId = board.id
outCard.parentId = board.id

// Card properties, skip first key which is the title
for (const key of keys.slice(1)) {
const value = row[key]
if(key === "Steps" && testrailFormat) {
const block = createTextBlock()
block.title = value
block.boardId = board.id
block.parentId = outCard.id
blocks.push(block)

outCard.fields.contentOrder = [block.id]
continue
}
if (!value) {
// Skip empty values
continue
}

const cardProperty = board.cardProperties.find((o) => o.name === key)!
let option = cardProperty.options.find((o) => o.value === value)
if (!option) {
const color = optionColors[optionColorIndex % optionColors.length]
optionColorIndex += 1
option = {
id: Utils.createGuid(),
value,
color: color,
}
cardProperty.options.push(option)
}

outCard.fields.properties[cardProperty.id] = option.id
}

blocks.push(outCard)
})

console.log('')
console.log(`Found ${input.length} card(s).`)

return [boards, blocks]
}

function getColumns(input: any[]) {
const row = input[0]
const keys = Object.keys(row)
// The first key (column) is the card title
return keys.slice(1)
}

function showHelp() {
console.log('import -i <input.csv> -o [output.boardarchive]')
exit(1)
}

main()
Loading