Skip to content

Commit 07d53d6

Browse files
committed
done!
1 parent f55c874 commit 07d53d6

File tree

5 files changed

+260
-30
lines changed

5 files changed

+260
-30
lines changed

README.md

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1+
This module supports conversion of the following.
2+
3+
- Lists(to Sections with board view, exclude archived)
4+
- Cards(to Tasks, exclude archived)
5+
- Checklists(to SubTasks)
6+
- Comments(noted author name. asana doesn't support switch author.)
7+
- Assignee, Followers
8+
- Attachments
9+
110
# How to Use
211

3-
1. Export Your Trello Board Backup file as JSON
12+
1. Exports Your Trello Board Backup file as JSON
13+
14+
2. Creates Trello Token
15+
- https://trello.com/app-key
416

5-
2. Create Personal Token in Asana Connect
17+
3. Creates Personal Token in Asana Connect
618
- https://asana.com/guide/help/api/api#gl-connect
719

8-
3. Edit Configuration for matching members, select asana team, workspace and so on.
20+
4. Makes Configuration for matching members, select asana team, workspace and so on. You can easily use `-m` option for finding members. You should rename `config.json.sample` file to `config.json`.

config.json.sample

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"trello": {
3+
"key": "<TRELLO_API_KEY>",
4+
"token": "<TRELLO_API_TOKEN>"
5+
},
6+
"asana": {
7+
"personal_access_token": "<ASANA_API_TOKEN>",
8+
"workspace": "<ASANA_WORKSPACE_ID>",
9+
"team": "<ASANA_TEAM_ID>"
10+
},
11+
"member": {
12+
"<TRELLO_MEMBER_ID>": "<ASANA_MEMBER_ID>"
13+
}
14+
}

index.js

Lines changed: 217 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ var Promise = require("bluebird");
44
var _ = require('underscore');
55
var package = fs.readJsonSync('package.json');
66
var Asana = require('asana');
7+
var Trello = require("node-trello");
8+
var request = require('request');
9+
var path = require('path');
710

811
var LABEL_COLOR = {
912
green: 'light-green',
@@ -77,9 +80,82 @@ var getUniqueName = function getUniqueName(name, haystack) {
7780
}
7881
};
7982

83+
var convertMap = function convertToMap(data, map) {
84+
if (_.isArray(data)) {
85+
return _.compact(_.map(data, id => {
86+
return convertToMap(id, map);
87+
}));
88+
}
89+
90+
if (typeof map[data] !== 'undefined') {
91+
return map[data];
92+
} else {
93+
return null;
94+
}
95+
};
96+
97+
var fetchImage = function (url) {
98+
return new Promise(function (resolve, reject) {
99+
request.get({
100+
url: url,
101+
encoding: null
102+
}, function (err, res, body) {
103+
if (err || res.statusCode !== 200) {
104+
reject(err || res.statusCode);
105+
return;
106+
}
107+
108+
if (body) {
109+
resolve(body);
110+
} else {
111+
resolve(null);
112+
}
113+
});
114+
});
115+
};
116+
80117
fs.readJson(opts.config).then(function (config) {
81118
var client = Asana.Client.create().useAccessToken(config.asana.personal_access_token);
82-
var projects = [];
119+
var trello = new Trello(config.trello.key, config.trello.token);
120+
var asanaData = {
121+
projects: [],
122+
tags: [],
123+
users: []
124+
};
125+
126+
var uploadImageToAsana = function (taskId, file, filename) {
127+
return new Promise(function (resolve, reject) {
128+
request.post({
129+
url: `https://app.asana.com/api/1.0/tasks/${taskId}/attachments`,
130+
headers: {
131+
Authorization: `Bearer ${config.asana.personal_access_token}`
132+
},
133+
formData: {
134+
file: {
135+
value: file,
136+
options: {
137+
filename: filename
138+
}
139+
}
140+
}
141+
}, function (err, res, body) {
142+
if (err || res.statusCode !== 200) {
143+
reject(err || res.statusCode);
144+
return;
145+
}
146+
147+
if (body) {
148+
try {
149+
body = JSON.parse(body);
150+
} catch (e) {}
151+
}
152+
153+
resolve(body);
154+
});
155+
});
156+
};
157+
158+
Promise.promisifyAll(trello);
83159

84160
if (!config.asana.workspace) {
85161
console.log('You should select your workspace in asana.');
@@ -107,31 +183,37 @@ fs.readJson(opts.config).then(function (config) {
107183
});
108184
}
109185

110-
return client.projects.findByTeam(config.asana.team).then(fetch).then(results => {
111-
projects = results;
112-
186+
// Prepare asana data to avoid duplicated
187+
return Promise.join(
188+
client.projects.findByTeam(config.asana.team).then(fetch),
189+
client.tags.findByWorkspace(config.asana.workspace).then(fetch),
190+
client.users.findByWorkspace(config.asana.workspace).then(fetch),
191+
(projects, tags, users) => {
192+
asanaData.projects = projects;
193+
asanaData.tags = tags;
194+
asanaData.users = users;
195+
}
196+
).then(function () {
113197
return Promise.map(opts.files, parseJson);
114198
}).then(function (files) {
199+
var trellMembers = _.flatten(_.pluck(files, 'members'));
200+
115201
// Check only member list
116202
if (opts.onlyMembers) {
117-
var members = _.flatten(_.pluck(files, 'members'));
118-
119203
console.log('Trello Users');
120-
console.log('<username>: <FullName>');
121-
console.log(_.map(members, function (member) {
122-
return `${member.username}: ${member.fullName}`;
204+
console.log('<id>: <FullName>(<username>)');
205+
console.log(_.map(trellMembers, function (member) {
206+
return `${member.id}: ${member.fullName}(${member.username})`;
123207
}).join('\n'));
124208

125209
console.log('\nAsana Users');
126210
console.log('<id>: <Name>');
127211

128-
return client.users.findByWorkspace(config.asana.workspace).then(fetch).then(users => {
129-
_.each(users, user => {
130-
console.log(`${user.id}: ${user.name}`);
131-
});
132-
133-
throw Promise.CancellationError;
212+
_.each(asanaData.users, user => {
213+
console.log(`${user.id}: ${user.name}`);
134214
});
215+
216+
throw Promise.CancellationError;
135217
}
136218

137219
// Executes in order
@@ -140,15 +222,26 @@ fs.readJson(opts.config).then(function (config) {
140222
let listToSectionMap = {};
141223
let cardToTaskMap = {};
142224
let labelToTagMap = {};
225+
let checklistMap = {};
226+
let userMap = {};
227+
228+
_.each(file.checklists, checklist => {
229+
checklistMap[checklist.id] = checklist;
230+
});
231+
232+
_.each(asanaData.users, user => {
233+
userMap[user.id] = user.name;
234+
});
143235

144236
// Creates a Project
145237
return client.projects.createInTeam(config.asana.team, {
146-
name: getUniqueName(file.name, _.pluck(projects, 'name')),
238+
name: getUniqueName(file.name, _.pluck(asanaData.projects, 'name')),
147239
notes: file.desc,
148240
layout: 'board'
149241
}).then(result => {
150242
console.log(`Created ${result.name} project in your team.`);
151243
projectData = result;
244+
asanaData.projects.push(result);
152245

153246
// Creates sections in order
154247
return Promise.mapSeries(file.lists, list => {
@@ -160,26 +253,124 @@ fs.readJson(opts.config).then(function (config) {
160253
});
161254
});
162255
}).then(() => {
256+
// Filter exists tags same with label
257+
var labels = _.filter(file.labels, label => {
258+
var matchedTag = _.find(asanaData.tags, tag => {
259+
return tag.name === label.name;
260+
});
261+
262+
if (matchedTag) {
263+
labelToTagMap[label.id] = matchedTag.id;
264+
return false;
265+
} else {
266+
return true;
267+
}
268+
});
269+
163270
// Creates tags
164-
return Promise.map(file.labels, label => {
165-
return client.tags.create({
271+
console.log(`Creating ${labels.length} tags...`);
166272

273+
return Promise.map(labels, label => {
274+
return client.tags.createInWorkspace(config.asana.workspace, {
275+
name: label.name,
276+
color: LABEL_COLOR[label.color],
277+
notes: 'Created by Trello'
167278
}).then(result => {
168-
279+
labelToTagMap[label.id] = result.id;
280+
asanaData.tags.push(result);
281+
console.log(`Created ${result.name}(${result.id}) tag.`);
169282
});
170-
});
283+
}, {
284+
concurrency: 3
285+
}).then(function () {
286+
console.log(`Creating ${file.cards.length} tasks...`);
287+
let countTask = 0;
288+
289+
// Creates tasks
290+
return Promise.mapSeries(file.cards, card => {
291+
return client.tasks.create({
292+
assignee: card.idMembers.length ? convertMap(_.first(card.idMembers), config.member) : null,
293+
due_at: card.due,
294+
followers: card.idMembers.length > 1 ? convertMap(card.idMembers, config.member) : [],
295+
name: card.name,
296+
notes: card.desc,
297+
memberships: [{
298+
project: projectData.id,
299+
section: convertMap(card.idList, listToSectionMap)
300+
}],
301+
tags: card.idLabels.length ? convertMap(card.idLabels, labelToTagMap) : [],
302+
projects: [ projectData.id ]
303+
}).then(result => {
304+
var promises = [];
305+
var taskData = result;
306+
cardToTaskMap[card.id] = result.id;
307+
countTask++;
308+
309+
if (countTask % 10 === 0) {
310+
console.log(`${countTask}...`);
311+
}
312+
313+
if (card.idChecklists.length) {
314+
promises.push(
315+
Promise.mapSeries(convertMap(card.idChecklists.reverse(), checklistMap), checklist => {
316+
return Promise.mapSeries(checklist.checkItems.reverse(), item => {
317+
return client.tasks.addSubtask(taskData.id, {
318+
name: item.name,
319+
completed: item.state !== 'incomplete'
320+
});
321+
}).then(function () {
322+
return client.tasks.addSubtask(taskData.id, {
323+
name: `${checklist.name}:`
324+
});
325+
});
326+
})
327+
);
328+
}
329+
330+
if (parseInt(card.badges.comments, 10) > 0) {
331+
promises.push(
332+
// Trello export has limitation for count of actions as 1000. so we need to request directly trello API.
333+
trello.getAsync(`/1/cards/${card.id}/actions?limit=1000`).then(result => {
334+
var comments = _.filter(result, action => {
335+
return action.type === 'commentCard';
336+
});
337+
338+
return Promise.mapSeries(comments.reverse(), comment => {
339+
var member = convertMap(comment.idMemberCreator, config.member);
340+
var text = comment.data.text;
341+
var memberName = member ? convertMap(member, userMap) : comment.memberCreator.fullName;
171342

172-
console.log(`Creating ${file.cards.length} parent tasks...`);
343+
text = `${memberName}: ${text} from Trello`;
173344

174-
// Creates tasks
175-
return Promise.map(file.cards, card => {
345+
return client.tasks.addComment(taskData.id, {
346+
text: text
347+
});
348+
});
349+
})
350+
);
351+
}
176352

177-
}, { concurrency: 3 });
353+
if (card.attachments.length) {
354+
promises.push(
355+
Promise.mapSeries(card.attachments, attachment => {
356+
return fetchImage(attachment.url).then(image => {
357+
return uploadImageToAsana(taskData.id, image, path.basename(attachment.url));
358+
});
359+
})
360+
);
361+
}
362+
363+
return Promise.all(promises);
364+
});
365+
});
366+
});
367+
}).then(function () {
368+
console.log('complete!');
178369
});
179370
});
180371
});
372+
}).catch(reason => {
373+
console.error(reason);
181374
}).catch(Promise.CancellationError, function (reason) {
182375
// nothing to do
183-
}).catch(function (reason) {
184-
console.error(reason);
185376
});

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"asana": "git+https://github.com/davidshimjs/node-asana.git",
2121
"bluebird": "^3.5.1",
2222
"fs-extra": "^4.0.2",
23+
"node-trello": "^1.3.0",
2324
"nomnom": "^1.8.1",
25+
"request": "^2.83.0",
2426
"underscore": "^1.8.3"
2527
}
2628
}

yarn.lock

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,13 @@ mime-types@^2.1.12, mime-types@~2.1.17:
263263
dependencies:
264264
mime-db "~1.30.0"
265265

266+
node-trello@^1.3.0:
267+
version "1.3.0"
268+
resolved "https://registry.yarnpkg.com/node-trello/-/node-trello-1.3.0.tgz#974aa251c89a5e4c97ff78a3a91cbb7bf5d7a2b5"
269+
dependencies:
270+
oauth "^0.9.15"
271+
request "^2.81.0"
272+
266273
nomnom@^1.8.1:
267274
version "1.8.1"
268275
resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
@@ -274,6 +281,10 @@ oauth-sign@~0.8.2:
274281
version "0.8.2"
275282
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
276283

284+
oauth@^0.9.15:
285+
version "0.9.15"
286+
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
287+
277288
performance-now@^2.1.0:
278289
version "2.1.0"
279290
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -286,7 +297,7 @@ qs@~6.5.1:
286297
version "6.5.1"
287298
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
288299

289-
request@^2.45.0:
300+
request@^2.45.0, request@^2.81.0, request@^2.83.0:
290301
version "2.83.0"
291302
resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
292303
dependencies:

0 commit comments

Comments
 (0)