1
0
mirror of https://github.com/mattermost/focalboard.git synced 2025-01-23 18:34:02 +02:00

Asana importer

This commit is contained in:
Chen-I Lim 2021-02-17 12:03:45 -08:00
parent a6920d9bad
commit 55c90a4379
9 changed files with 1696 additions and 3 deletions

View File

@ -1,4 +1,4 @@
# Import scripts
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there is an example of importing from Trello. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
This subfolder contains scripts to import data from other systems. It is at an early stage. At present, there are examples of importing from Trello and Asana. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.

View File

@ -1,4 +1,12 @@
# Asana importer
Placeholder for now. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.
This node app converts a Trello json archive into a Focalboard archive. To use:
1. From the Asana Board Menu, select `Export / Print`, and `JSON`
2. Save it locally, e.g. to `asana.json`
3. Run `npm install`
4. Run `npx ts-node importAsana.ts -i <asana.json> -o archive.focalboard`
5. In Focalboard, click `Settings`, then `Import archive` and select `archive.focalboard`
## Import scope
Currently, the script imports all cards from a single board, including their section (column) membership, names, and notes. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.

126
import/asana/asana.ts Normal file
View File

@ -0,0 +1,126 @@
// Generated by https://quicktype.io
//
// To change quicktype's target language, run command:
//
// "Set quicktype target language"
export interface Asana {
data: Datum[];
}
export interface Datum {
gid: string;
assignee: null;
assignee_status: AssigneeStatus;
completed: boolean;
completed_at: null;
created_at: string;
custom_fields: CustomField[];
due_at: null;
due_on: null;
followers: Workspace[];
hearted: boolean;
hearts: any[];
liked: boolean;
likes: any[];
memberships: Membership[];
modified_at: string;
name: string;
notes: string;
num_hearts: number;
num_likes: number;
parent: Workspace | null;
permalink_url: string;
projects: Workspace[];
resource_type: WorkspaceResourceType;
start_on: null;
subtasks: Datum[];
tags: any[];
resource_subtype: ResourceSubtype;
workspace: Workspace;
}
export enum AssigneeStatus {
Upcoming = "upcoming",
}
export interface CustomField {
gid: string;
enabled: boolean;
enum_options: Enum[];
enum_value: Enum | null;
name: CustomFieldName;
created_by: null;
resource_subtype: Type;
resource_type: CustomFieldResourceType;
type: Type;
}
export interface Enum {
gid: string;
color: Color;
enabled: boolean;
name: EnumOptionName;
resource_type: EnumOptionResourceType;
}
export enum Color {
Blue = "blue",
BlueGreen = "blue-green",
CoolGray = "cool-gray",
Orange = "orange",
Red = "red",
Yellow = "yellow",
YellowOrange = "yellow-orange",
}
export enum EnumOptionName {
Deferred = "Deferred",
Done = "Done",
High = "High",
InProgress = "In Progress",
Low = "Low",
Medium = "Medium",
NotStarted = "Not Started",
Waiting = "Waiting",
}
export enum EnumOptionResourceType {
EnumOption = "enum_option",
}
export enum CustomFieldName {
Priority = "Priority",
TaskProgress = "Task Progress",
}
export enum Type {
Enum = "enum",
}
export enum CustomFieldResourceType {
CustomField = "custom_field",
}
export interface Workspace {
gid: string;
name: string;
resource_type: WorkspaceResourceType;
}
export enum WorkspaceResourceType {
Project = "project",
Section = "section",
Task = "task",
User = "user",
Workspace = "workspace",
}
export interface Membership {
project: Workspace;
section: Workspace;
}
export enum ResourceSubtype {
DefaultTask = "default_task",
}

174
import/asana/importAsana.ts Normal file
View File

@ -0,0 +1,174 @@
import * as fs from 'fs'
import minimist from 'minimist'
import {exit} from 'process'
import {IArchive} from '../../webapp/src/blocks/archive'
import {IBlock} from '../../webapp/src/blocks/block'
import {IPropertyOption, IPropertyTemplate, MutableBoard} from '../../webapp/src/blocks/board'
import {MutableBoardView} from '../../webapp/src/blocks/boardView'
import {MutableCard} from '../../webapp/src/blocks/card'
import {MutableTextBlock} from '../../webapp/src/blocks/textBlock'
import {Asana, Workspace} from './asana'
import {Utils} from './utils'
// HACKHACK: To allow Utils.CreateGuid to work
(global.window as any) = {}
function main() {
const args: minimist.ParsedArgs = minimist(process.argv.slice(2))
const inputFile = args['i']
const outputFile = args['o'] || 'archive.focalboard'
if (!inputFile) {
showHelp()
}
// Read input
const inputData = fs.readFileSync(inputFile, 'utf-8')
const input = JSON.parse(inputData) as Asana
// Convert
const output = convert(input)
// Save output
const outputData = JSON.stringify(output)
fs.writeFileSync(outputFile, outputData)
console.log(`Exported to ${outputFile}`)
}
function getProjects(input: Asana): Workspace[] {
const projectMap = new Map<string, Workspace>()
input.data.forEach(datum => {
datum.projects.forEach(project => {
if (!projectMap.get(project.gid)) {
projectMap.set(project.gid, project)
}
})
})
return [...projectMap.values()]
}
function getSections(input: Asana, projectId: string): Workspace[] {
const sectionMap = new Map<string, Workspace>()
input.data.forEach(datum => {
const membership = datum.memberships.find(o => o.project.gid === projectId)
if (membership) {
if (!sectionMap.get(membership.section.gid)) {
sectionMap.set(membership.section.gid, membership.section)
}
}
})
return [...sectionMap.values()]
}
function convert(input: Asana): IArchive {
const archive: IArchive = {
version: 1,
date: Date.now(),
blocks: []
}
const projects = getProjects(input)
if (projects.length < 1) {
console.error('No projects found')
return archive
}
// TODO: Handle multiple projects
const project = projects[0]
const blocks: IBlock[] = []
// Board
const board = new MutableBoard()
console.log(`Board: ${project.name}`)
board.rootId = board.id
board.title = project.name
// Convert sections (columns) to a Select property
const optionIdMap = new Map<string, string>()
const options: IPropertyOption[] = []
const sections = getSections(input, project.gid)
sections.forEach(section => {
const optionId = Utils.createGuid()
optionIdMap.set(section.gid, optionId)
const option: IPropertyOption = {
id: optionId,
value: section.name,
color: 'propColorDefault',
}
options.push(option)
})
const cardProperty: IPropertyTemplate = {
id: Utils.createGuid(),
name: 'Section',
type: 'select',
options
}
board.cardProperties = [cardProperty]
blocks.push(board)
// Board view
const view = new MutableBoardView()
view.title = 'Board View'
view.viewType = 'board'
view.rootId = board.id
view.parentId = board.id
blocks.push(view)
// Cards
input.data.forEach(card => {
console.log(`Card: ${card.name}`)
const outCard = new MutableCard()
outCard.title = card.name
outCard.rootId = board.id
outCard.parentId = board.id
// Map lists to Select property options
const membership = card.memberships.find(o => o.project.gid === project.gid)
if (membership) {
const optionId = optionIdMap.get(membership.section.gid)
if (optionId) {
outCard.properties[cardProperty.id] = optionId
} else {
console.warn(`Invalid idList: ${membership.section.gid} for card: ${card.name}`)
}
} else {
console.warn(`Missing idList for card: ${card.name}`)
}
blocks.push(outCard)
if (card.notes) {
// console.log(`\t${card.notes}`)
const text = new MutableTextBlock()
text.title = card.notes
text.rootId = board.id
text.parentId = outCard.id
blocks.push(text)
outCard.contentOrder = [text.id]
}
})
archive.blocks = blocks
console.log('')
console.log(`Found ${input.data.length} card(s).`)
return archive
}
function showHelp() {
console.log('import -i <input.json> -o [output.focalboard]')
exit(-1)
}
main()

1317
import/asana/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

24
import/asana/package.json Normal file
View File

@ -0,0 +1,24 @@
{
"name": "focalboard-asana-importer",
"version": "1.0.0",
"private": true,
"description": "",
"main": "importAsana.js",
"scripts": {
"test": "ts-node importAsana.ts -i test/asana.json -o test/archive.focalboard"
},
"keywords": [],
"author": "",
"devDependencies": {
"@types/minimist": "^1.2.1",
"@types/node": "^14.14.28",
"@typescript-eslint/eslint-plugin": "^4.15.0",
"@typescript-eslint/parser": "^4.15.0",
"eslint": "^7.20.0",
"ts-node": "^9.1.1",
"typescript": "^4.1.5"
},
"dependencies": {
"minimist": "^1.2.5"
}
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"jsx": "react",
"target": "es2019",
"module": "commonjs",
"esModuleInterop": true,
"noImplicitAny": true,
"strict": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"sourceMap": true,
"allowJs": true,
"resolveJsonModule": true,
"incremental": false,
"outDir": "./dist",
"moduleResolution": "node"
},
"include": [
"."
],
"exclude": [
".git",
"**/node_modules/*",
"dist",
"pack"
]
}

17
import/asana/utils.ts Normal file
View File

@ -0,0 +1,17 @@
import * as crypto from 'crypto'
class Utils {
static createGuid(): string {
function randomDigit() {
if (crypto && crypto.randomBytes) {
const rands = crypto.randomBytes(1)
return (rands[0] % 16).toString(16)
}
return (Math.floor((Math.random() * 16))).toString(16)
}
return 'xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx'.replace(/x/g, randomDigit)
}
}
export { Utils }

View File

@ -9,6 +9,6 @@ This node app converts a Trello json archive into a Focalboard archive. To use:
## Import scope
Currently, the script imports all cards from a single board, including their list (column) membership, names, and descriptions.
Currently, the script imports all cards from a single board, including their list (column) membership, names, and descriptions. [Contribute code](https://www.focalboard.com/contribute/getting-started/) to expand this.