import.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. const DateString = Match.Where(function (dateAsString) {
  2. check(dateAsString, String);
  3. return moment(dateAsString, moment.ISO_8601).isValid();
  4. });
  5. class TrelloCreator {
  6. constructor() {
  7. // the object creation dates, indexed by Trello id (so we only parse actions once!)
  8. this.createdAt = {
  9. board: null,
  10. cards: {},
  11. lists: {},
  12. };
  13. // the labels we created, indexed by Trello id (to map when importing cards)
  14. this.labels = {};
  15. // the lists we created, indexed by Trello id (to map when importing cards)
  16. this.lists = {};
  17. // the comments, indexed by Trello card id (to map when importing cards)
  18. this.comments = {};
  19. }
  20. checkActions(trelloActions) {
  21. check(trelloActions, [Match.ObjectIncluding({
  22. data: Object,
  23. date: DateString,
  24. type: String,
  25. })]);
  26. // XXX perform deeper checks based on type
  27. }
  28. checkBoard(trelloBoard) {
  29. check(trelloBoard, Match.ObjectIncluding({
  30. closed: Boolean,
  31. labels: [Match.ObjectIncluding({
  32. // XXX check versus list
  33. color: String,
  34. name: String,
  35. })],
  36. name: String,
  37. prefs: Match.ObjectIncluding({
  38. // XXX check versus list
  39. background: String,
  40. // XXX check versus list
  41. permissionLevel: String,
  42. }),
  43. }));
  44. }
  45. checkLists(trelloLists) {
  46. check(trelloLists, [Match.ObjectIncluding({
  47. closed: Boolean,
  48. name: String,
  49. })]);
  50. }
  51. checkCards(trelloCards) {
  52. check(trelloCards, [Match.ObjectIncluding({
  53. closed: Boolean,
  54. desc: String,
  55. // XXX check idLabels
  56. name: String,
  57. pos: Number,
  58. })]);
  59. }
  60. /**
  61. * must call parseActions before calling this one
  62. */
  63. createBoardAndLabels(trelloBoard) {
  64. const createdAt = this.createdAt.board;
  65. const boardToCreate = {
  66. archived: trelloBoard.closed,
  67. color: this.getColor(trelloBoard.prefs.background),
  68. createdAt,
  69. labels: [],
  70. members: [{
  71. userId: Meteor.userId(),
  72. isAdmin: true,
  73. isActive: true,
  74. }],
  75. // current mapping is easy as trello and wekan use same keys: 'private' and 'public'
  76. permission: trelloBoard.prefs.permissionLevel,
  77. slug: getSlug(trelloBoard.name) || 'board',
  78. stars: 0,
  79. title: trelloBoard.name,
  80. };
  81. trelloBoard.labels.forEach((label) => {
  82. const labelToCreate = {
  83. _id: Random.id(6),
  84. color: label.color,
  85. name: label.name,
  86. };
  87. // we need to remember them by Trello ID, as this is the only ref we have when importing cards
  88. this.labels[label.id] = labelToCreate;
  89. boardToCreate.labels.push(labelToCreate);
  90. });
  91. const now = new Date();
  92. const boardId = Boards.direct.insert(boardToCreate);
  93. Boards.direct.update(boardId, {$set: {modifiedAt: now}});
  94. // log activity
  95. Activities.direct.insert({
  96. activityType: 'importBoard',
  97. boardId,
  98. createdAt: now,
  99. source: {
  100. id: trelloBoard.id,
  101. system: 'Trello',
  102. url: trelloBoard.url,
  103. },
  104. // we attribute the import to current user, not the one from the original object
  105. userId: Meteor.userId(),
  106. });
  107. return boardId;
  108. }
  109. createLists(trelloLists, boardId) {
  110. trelloLists.forEach((list) => {
  111. const listToCreate = {
  112. archived: list.closed,
  113. boardId,
  114. createdAt: this.createdAt.lists[list.id],
  115. title: list.name,
  116. userId: Meteor.userId(),
  117. };
  118. const listId = Lists.direct.insert(listToCreate);
  119. const now = new Date();
  120. Lists.direct.update(listId, {$set: {'updatedAt': now}});
  121. listToCreate._id = listId;
  122. this.lists[list.id] = listToCreate;
  123. // log activity
  124. Activities.direct.insert({
  125. activityType: 'importList',
  126. boardId,
  127. createdAt: now,
  128. listId,
  129. source: {
  130. id: list.id,
  131. system: 'Trello',
  132. },
  133. // we attribute the import to current user, not the one from the original object
  134. userId: Meteor.userId(),
  135. });
  136. });
  137. }
  138. createCardsAndComments(trelloCards, boardId) {
  139. trelloCards.forEach((card) => {
  140. const cardToCreate = {
  141. archived: card.closed,
  142. boardId,
  143. createdAt: this.createdAt.cards[card.id],
  144. dateLastActivity: new Date(),
  145. description: card.desc,
  146. listId: this.lists[card.idList]._id,
  147. sort: card.pos,
  148. title: card.name,
  149. // XXX use the original user?
  150. userId: Meteor.userId(),
  151. };
  152. // add labels
  153. if(card.idLabels) {
  154. cardToCreate.labelIds = card.idLabels.map((trelloId) => {
  155. return this.labels[trelloId]._id;
  156. });
  157. }
  158. // insert card
  159. const cardId = Cards.direct.insert(cardToCreate);
  160. // log activity
  161. Activities.direct.insert({
  162. activityType: 'importCard',
  163. boardId,
  164. cardId,
  165. createdAt: new Date(),
  166. listId: cardToCreate.listId,
  167. source: {
  168. id: card.id,
  169. system: 'Trello',
  170. url: card.url,
  171. },
  172. // we attribute the import to current user, not the one from the original card
  173. userId: Meteor.userId(),
  174. });
  175. // add comments
  176. const comments = this.comments[card.id];
  177. if(comments) {
  178. comments.forEach((comment) => {
  179. const commentToCreate = {
  180. boardId,
  181. cardId,
  182. createdAt: comment.date,
  183. text: comment.data.text,
  184. // XXX use the original comment user instead
  185. userId: Meteor.userId(),
  186. };
  187. // dateLastActivity will be set from activity insert, no need to update it ourselves
  188. const commentId = CardComments.direct.insert(commentToCreate);
  189. Activities.direct.insert({
  190. activityType: 'addComment',
  191. boardId: commentToCreate.boardId,
  192. cardId: commentToCreate.cardId,
  193. commentId,
  194. createdAt: commentToCreate.createdAt,
  195. userId: commentToCreate.userId,
  196. });
  197. });
  198. }
  199. // XXX add attachments
  200. });
  201. }
  202. getColor(trelloColorCode) {
  203. // trello color name => wekan color
  204. const mapColors = {
  205. 'blue': 'belize',
  206. 'orange': 'pumpkin',
  207. 'green': 'nephritis',
  208. 'red': 'pomegranate',
  209. 'purple': 'wisteria',
  210. 'pink': 'pomegranate',
  211. 'lime': 'nephritis',
  212. 'sky': 'belize',
  213. 'grey': 'midnight',
  214. };
  215. const wekanColor = mapColors[trelloColorCode];
  216. return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0];
  217. }
  218. parseActions(trelloActions) {
  219. trelloActions.forEach((action) => {
  220. switch (action.type) {
  221. case 'createBoard':
  222. this.createdAt.board = action.date;
  223. break;
  224. case 'createCard':
  225. const cardId = action.data.card.id;
  226. this.createdAt.cards[cardId] = action.date;
  227. break;
  228. case 'createList':
  229. const listId = action.data.list.id;
  230. this.createdAt.lists[listId] = action.date;
  231. break;
  232. case 'commentCard':
  233. const id = action.data.card.id;
  234. if(this.comments[id]) {
  235. this.comments[id].push(action);
  236. } else {
  237. this.comments[id] = [action];
  238. }
  239. break;
  240. default:
  241. // do nothing
  242. break;
  243. }
  244. });
  245. }
  246. }
  247. Meteor.methods({
  248. importTrelloBoard(trelloBoard, data) {
  249. const trelloCreator = new TrelloCreator();
  250. // 1. check all parameters are ok from a syntax point of view
  251. try {
  252. // we don't use additional data - this should be an empty object
  253. check(data, {});
  254. trelloCreator.checkActions(trelloBoard.actions);
  255. trelloCreator.checkBoard(trelloBoard);
  256. trelloCreator.checkLists(trelloBoard.lists);
  257. trelloCreator.checkCards(trelloBoard.cards);
  258. } catch(e) {
  259. throw new Meteor.Error('error-json-schema');
  260. }
  261. // 2. check parameters are ok from a business point of view (exist & authorized)
  262. // nothing to check, everyone can import boards in their account
  263. // 3. create all elements
  264. trelloCreator.parseActions(trelloBoard.actions);
  265. const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
  266. trelloCreator.createLists(trelloBoard.lists, boardId);
  267. trelloCreator.createCardsAndComments(trelloBoard.cards, boardId);
  268. // XXX add members
  269. return boardId;
  270. },
  271. importTrelloCard(trelloCard, data) {
  272. // 1. check parameters are ok from a syntax point of view
  273. const DateString = Match.Where(function (dateAsString) {
  274. check(dateAsString, String);
  275. return moment(dateAsString, moment.ISO_8601).isValid();
  276. });
  277. try {
  278. check(trelloCard, Match.ObjectIncluding({
  279. name: String,
  280. desc: String,
  281. closed: Boolean,
  282. dateLastActivity: DateString,
  283. labels: [Match.ObjectIncluding({
  284. name: String,
  285. color: String,
  286. })],
  287. actions: [Match.ObjectIncluding({
  288. type: String,
  289. date: DateString,
  290. data: Object,
  291. })],
  292. members: [Object],
  293. }));
  294. check(data, {
  295. listId: String,
  296. sortIndex: Number,
  297. });
  298. } catch(e) {
  299. throw new Meteor.Error('error-json-schema');
  300. }
  301. // 2. check parameters are ok from a business point of view (exist & authorized)
  302. const list = Lists.findOne(data.listId);
  303. if(!list) {
  304. throw new Meteor.Error('error-list-doesNotExist');
  305. }
  306. if(Meteor.isServer) {
  307. if (!allowIsBoardMember(Meteor.userId(), Boards.findOne(list.boardId))) {
  308. throw new Meteor.Error('error-board-notAMember');
  309. }
  310. }
  311. // 3. map all fields for the card to create
  312. const dateOfImport = new Date();
  313. const cardToCreate = {
  314. archived: trelloCard.closed,
  315. boardId: list.boardId,
  316. // this is a default date, we'll fetch the actual one from the actions array
  317. createdAt: dateOfImport,
  318. dateLastActivity: dateOfImport,
  319. description: trelloCard.desc,
  320. listId: list._id,
  321. sort: data.sortIndex,
  322. title: trelloCard.name,
  323. // XXX use the original user?
  324. userId: Meteor.userId(),
  325. };
  326. // 4. find actual creation date
  327. const creationAction = trelloCard.actions.find((action) => {
  328. return action.type === 'createCard';
  329. });
  330. if(creationAction) {
  331. cardToCreate.createdAt = creationAction.date;
  332. }
  333. // 5. map labels - create missing ones
  334. trelloCard.labels.forEach((currentLabel) => {
  335. const color = currentLabel.color;
  336. const name = currentLabel.name;
  337. const existingLabel = list.board().getLabel(name, color);
  338. let labelId = undefined;
  339. if (existingLabel) {
  340. labelId = existingLabel._id;
  341. } else {
  342. let labelCreated = list.board().addLabel(name, color);
  343. // XXX currently mutations return no value so we have to fetch the label we just created
  344. // waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
  345. labelCreated = list.board().getLabel(name, color);
  346. labelId = labelCreated._id;
  347. }
  348. if(labelId) {
  349. if (!cardToCreate.labelIds) {
  350. cardToCreate.labelIds = [];
  351. }
  352. cardToCreate.labelIds.push(labelId);
  353. }
  354. });
  355. // 6. insert new card into list
  356. const cardId = Cards.direct.insert(cardToCreate);
  357. Activities.direct.insert({
  358. activityType: 'importCard',
  359. boardId: cardToCreate.boardId,
  360. cardId,
  361. createdAt: dateOfImport,
  362. listId: cardToCreate.listId,
  363. source: {
  364. id: trelloCard.id,
  365. system: 'Trello',
  366. url: trelloCard.url,
  367. },
  368. // we attribute the import to current user, not the one from the original card
  369. userId: Meteor.userId(),
  370. });
  371. // 7. parse actions and add comments
  372. trelloCard.actions.forEach((currentAction) => {
  373. if(currentAction.type === 'commentCard') {
  374. const commentToCreate = {
  375. boardId: list.boardId,
  376. cardId,
  377. createdAt: currentAction.date,
  378. text: currentAction.data.text,
  379. // XXX use the original comment user instead
  380. userId: Meteor.userId(),
  381. };
  382. const commentId = CardComments.direct.insert(commentToCreate);
  383. Activities.direct.insert({
  384. activityType: 'addComment',
  385. boardId: commentToCreate.boardId,
  386. cardId: commentToCreate.cardId,
  387. commentId,
  388. createdAt: commentToCreate.createdAt,
  389. userId: commentToCreate.userId,
  390. });
  391. }
  392. });
  393. return cardId;
  394. },
  395. });