mirror of
https://github.com/mattermost/focalboard.git
synced 2025-01-23 18:34:02 +02:00
Asana importer
This commit is contained in:
parent
a6920d9bad
commit
55c90a4379
@ -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.
|
||||
|
||||
|
@ -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
126
import/asana/asana.ts
Normal 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
174
import/asana/importAsana.ts
Normal 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
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
24
import/asana/package.json
Normal 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"
|
||||
}
|
||||
}
|
27
import/asana/tsconfig.json
Normal file
27
import/asana/tsconfig.json
Normal 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
17
import/asana/utils.ts
Normal 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 }
|
@ -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.
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user