import.js 11 KB

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