wekanCreator.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. import {
  3. formatDateTime,
  4. formatDate,
  5. formatTime,
  6. getISOWeek,
  7. isValidDate,
  8. isBefore,
  9. isAfter,
  10. isSame,
  11. add,
  12. subtract,
  13. startOf,
  14. endOf,
  15. format,
  16. parseDate,
  17. now,
  18. createDate,
  19. fromNow,
  20. calendar
  21. } from '/imports/lib/dateUtils';
  22. const DateString = Match.Where(function(dateAsString) {
  23. check(dateAsString, String);
  24. return isValidDate(new Date(dateAsString));
  25. });
  26. export class WekanCreator {
  27. constructor(data) {
  28. // we log current date, to use the same timestamp for all our actions.
  29. // this helps to retrieve all elements performed by the same import.
  30. this._nowDate = new Date();
  31. // The object creation dates, indexed by Wekan id
  32. // (so we only parse actions once!)
  33. this.createdAt = {
  34. board: null,
  35. cards: {},
  36. lists: {},
  37. swimlanes: {},
  38. customFields: {},
  39. };
  40. // The object creator Wekan Id, indexed by the object Wekan id
  41. // (so we only parse actions once!)
  42. this.createdBy = {
  43. cards: {}, // only cards have a field for that
  44. };
  45. // Map of labels Wekan ID => Wekan ID
  46. this.labels = {};
  47. // Map of swimlanes Wekan ID => Wekan ID
  48. this.swimlanes = {};
  49. // Map of lists Wekan ID => Wekan ID
  50. this.lists = {};
  51. // Map of cards Wekan ID => Wekan ID
  52. this.cards = {};
  53. // Map of custom fields Wekan ID => Wekan ID
  54. this.customFields = {};
  55. // Map of comments Wekan ID => Wekan ID
  56. this.commentIds = {};
  57. // Map of attachments Wekan ID => Wekan ID
  58. this.attachmentIds = {};
  59. // Map of checklists Wekan ID => Wekan ID
  60. this.checklists = {};
  61. // Map of checklistItems Wekan ID => Wekan ID
  62. this.checklistItems = {};
  63. // The comments, indexed by Wekan card id (to map when importing cards)
  64. this.comments = {};
  65. // Map of rules Wekan ID => Wekan ID
  66. this.rules = {};
  67. // the members, indexed by Wekan member id => Wekan user ID
  68. this.members = data.membersMapping ? data.membersMapping : {};
  69. // Map of triggers Wekan ID => Wekan ID
  70. this.triggers = {};
  71. // Map of actions Wekan ID => Wekan ID
  72. this.actions = {};
  73. // maps a wekanCardId to an array of wekanAttachments
  74. this.attachments = {};
  75. }
  76. /**
  77. * If dateString is provided,
  78. * return the Date it represents.
  79. * If not, will return the date when it was first called.
  80. * This is useful for us, as we want all import operations to
  81. * have the exact same date for easier later retrieval.
  82. *
  83. * @param {String} dateString a properly formatted Date
  84. */
  85. _now(dateString) {
  86. if (dateString) {
  87. return new Date(dateString);
  88. }
  89. if (!this._nowDate) {
  90. this._nowDate = new Date();
  91. }
  92. return this._nowDate;
  93. }
  94. /**
  95. * if wekanUserId is provided and we have a mapping,
  96. * return it.
  97. * Otherwise return current logged user.
  98. * @param wekanUserId
  99. * @private
  100. */
  101. _user(wekanUserId) {
  102. if (wekanUserId && this.members[wekanUserId]) {
  103. return this.members[wekanUserId];
  104. }
  105. return Meteor.userId();
  106. }
  107. checkActivities(wekanActivities) {
  108. check(wekanActivities, [
  109. Match.ObjectIncluding({
  110. activityType: String,
  111. createdAt: DateString,
  112. }),
  113. ]);
  114. // XXX we could perform more thorough checks based on action type
  115. }
  116. checkBoard(wekanBoard) {
  117. check(
  118. wekanBoard,
  119. Match.ObjectIncluding({
  120. archived: Boolean,
  121. title: String,
  122. // XXX refine control by validating 'color' against a list of
  123. // allowed values (is it worth the maintenance?)
  124. color: String,
  125. permission: Match.Where(value => {
  126. return ['private', 'public'].indexOf(value) >= 0;
  127. }),
  128. }),
  129. );
  130. }
  131. checkCards(wekanCards) {
  132. check(wekanCards, [
  133. Match.ObjectIncluding({
  134. archived: Boolean,
  135. dateLastActivity: DateString,
  136. labelIds: [String],
  137. title: String,
  138. sort: Number,
  139. }),
  140. ]);
  141. }
  142. checkLabels(wekanLabels) {
  143. check(wekanLabels, [
  144. Match.ObjectIncluding({
  145. // XXX refine control by validating 'color' against a list of allowed
  146. // values (is it worth the maintenance?)
  147. color: String,
  148. }),
  149. ]);
  150. }
  151. checkLists(wekanLists) {
  152. check(wekanLists, [
  153. Match.ObjectIncluding({
  154. archived: Boolean,
  155. title: String,
  156. }),
  157. ]);
  158. }
  159. checkSwimlanes(wekanSwimlanes) {
  160. check(wekanSwimlanes, [
  161. Match.ObjectIncluding({
  162. archived: Boolean,
  163. title: String,
  164. }),
  165. ]);
  166. }
  167. checkChecklists(wekanChecklists) {
  168. check(wekanChecklists, [
  169. Match.ObjectIncluding({
  170. cardId: String,
  171. title: String,
  172. }),
  173. ]);
  174. }
  175. checkChecklistItems(wekanChecklistItems) {
  176. check(wekanChecklistItems, [
  177. Match.ObjectIncluding({
  178. cardId: String,
  179. title: String,
  180. }),
  181. ]);
  182. }
  183. checkRules(wekanRules) {
  184. check(wekanRules, [
  185. Match.ObjectIncluding({
  186. triggerId: String,
  187. actionId: String,
  188. title: String,
  189. }),
  190. ]);
  191. }
  192. checkTriggers(wekanTriggers) {
  193. // XXX More check based on trigger type
  194. check(wekanTriggers, [
  195. Match.ObjectIncluding({
  196. activityType: String,
  197. desc: String,
  198. }),
  199. ]);
  200. }
  201. getMembersToMap(data) {
  202. // we will work on the list itself (an ordered array of objects) when a
  203. // mapping is done, we add a 'wekan' field to the object representing the
  204. // imported member
  205. const membersToMap = data.members;
  206. const users = data.users;
  207. // auto-map based on username
  208. membersToMap.forEach(importedMember => {
  209. importedMember.id = importedMember.userId;
  210. delete importedMember.userId;
  211. const user = users.filter(user => {
  212. return user._id === importedMember.id;
  213. })[0];
  214. if (user.profile && user.profile.fullname) {
  215. importedMember.fullName = user.profile.fullname;
  216. }
  217. importedMember.username = user.username;
  218. const wekanUser = ReactiveCache.getUser({ username: importedMember.username });
  219. if (wekanUser) {
  220. importedMember.wekanId = wekanUser._id;
  221. }
  222. });
  223. return membersToMap;
  224. }
  225. checkActions(wekanActions) {
  226. // XXX More check based on action type
  227. check(wekanActions, [
  228. Match.ObjectIncluding({
  229. actionType: String,
  230. desc: String,
  231. }),
  232. ]);
  233. }
  234. // You must call parseActions before calling this one.
  235. createBoardAndLabels(boardToImport) {
  236. const boardToCreate = {
  237. archived: boardToImport.archived,
  238. color: boardToImport.color,
  239. // very old boards won't have a creation activity so no creation date
  240. createdAt: this._now(boardToImport.createdAt),
  241. labels: [],
  242. members: [
  243. {
  244. userId: Meteor.userId(),
  245. wekanId: Meteor.userId(),
  246. isActive: true,
  247. isAdmin: true,
  248. isNoComments: false,
  249. isCommentOnly: false,
  250. swimlaneId: false,
  251. },
  252. ],
  253. presentParentTask: boardToImport.presentParentTask,
  254. // Standalone Export has modifiedAt missing, adding modifiedAt to fix it
  255. modifiedAt: this._now(boardToImport.modifiedAt),
  256. permission: boardToImport.permission,
  257. slug: getSlug(boardToImport.title) || 'board',
  258. stars: 0,
  259. title: Boards.uniqueTitle(boardToImport.title),
  260. };
  261. // now add other members
  262. if (boardToImport.members) {
  263. boardToImport.members.forEach(wekanMember => {
  264. // is it defined and do we already have it in our list?
  265. if (
  266. wekanMember.wekanId &&
  267. !boardToCreate.members.some(
  268. member => member.wekanId === wekanMember.wekanId,
  269. )
  270. )
  271. boardToCreate.members.push({
  272. ...wekanMember,
  273. userId: wekanMember.wekanId,
  274. });
  275. });
  276. }
  277. if (boardToImport.labels) {
  278. boardToImport.labels.forEach(label => {
  279. const labelToCreate = {
  280. _id: Random.id(6),
  281. color: label.color,
  282. name: label.name,
  283. };
  284. // We need to remember them by Wekan ID, as this is the only ref we have
  285. // when importing cards.
  286. this.labels[label._id] = labelToCreate._id;
  287. boardToCreate.labels.push(labelToCreate);
  288. });
  289. }
  290. const boardId = Boards.direct.insert(boardToCreate);
  291. Boards.direct.update(boardId, {
  292. $set: {
  293. modifiedAt: this._now(),
  294. },
  295. });
  296. // log activity
  297. Activities.direct.insert({
  298. activityType: 'importBoard',
  299. boardId,
  300. createdAt: this._now(),
  301. source: {
  302. id: boardToImport.id,
  303. system: 'Wekan',
  304. },
  305. // We attribute the import to current user,
  306. // not the author from the original object.
  307. userId: this._user(),
  308. });
  309. return boardId;
  310. }
  311. /**
  312. * Create the Wekan cards corresponding to the supplied Wekan cards,
  313. * as well as all linked data: activities, comments, and attachments
  314. * @param wekanCards
  315. * @param boardId
  316. * @returns {Array}
  317. */
  318. createCards(wekanCards, boardId) {
  319. const result = [];
  320. wekanCards.forEach(card => {
  321. const cardToCreate = {
  322. archived: card.archived,
  323. boardId,
  324. // very old boards won't have a creation activity so no creation date
  325. createdAt: this._now(this.createdAt.cards[card._id]),
  326. dateLastActivity: this._now(),
  327. description: card.description,
  328. listId: this.lists[card.listId],
  329. swimlaneId: this.swimlanes[card.swimlaneId],
  330. sort: card.sort,
  331. title: card.title,
  332. // we attribute the card to its creator if available
  333. userId: this._user(this.createdBy.cards[card._id]),
  334. isOvertime: card.isOvertime || false,
  335. startAt: card.startAt ? this._now(card.startAt) : null,
  336. dueAt: card.dueAt ? this._now(card.dueAt) : null,
  337. spentTime: card.spentTime || null,
  338. };
  339. // add labels
  340. if (card.labelIds) {
  341. cardToCreate.labelIds = card.labelIds.map(wekanId => {
  342. return this.labels[wekanId];
  343. });
  344. }
  345. // add members {
  346. if (card.members) {
  347. const wekanMembers = [];
  348. // we can't just map, as some members may not have been mapped
  349. card.members.forEach(sourceMemberId => {
  350. if (this.members[sourceMemberId]) {
  351. const wekanId = this.members[sourceMemberId];
  352. // we may map multiple Wekan members to the same wekan user
  353. // in which case we risk adding the same user multiple times
  354. if (!wekanMembers.find(wId => wId === wekanId)) {
  355. wekanMembers.push(wekanId);
  356. }
  357. }
  358. return true;
  359. });
  360. if (wekanMembers.length > 0) {
  361. cardToCreate.members = wekanMembers;
  362. }
  363. }
  364. // add assignees
  365. if (card.assignees) {
  366. const wekanAssignees = [];
  367. // we can't just map, as some members may not have been mapped
  368. card.assignees.forEach(sourceMemberId => {
  369. if (this.members[sourceMemberId]) {
  370. const wekanId = this.members[sourceMemberId];
  371. // we may map multiple Wekan members to the same wekan user
  372. // in which case we risk adding the same user multiple times
  373. if (!wekanAssignees.find(wId => wId === wekanId)) {
  374. wekanAssignees.push(wekanId);
  375. }
  376. }
  377. return true;
  378. });
  379. if (wekanAssignees.length > 0) {
  380. cardToCreate.assignees = wekanAssignees;
  381. }
  382. }
  383. // set color
  384. if (card.color) {
  385. cardToCreate.color = card.color;
  386. }
  387. // add custom fields
  388. if (card.customFields) {
  389. cardToCreate.customFields = card.customFields.map(field => {
  390. return {
  391. _id: this.customFields[field._id],
  392. value: field.value,
  393. };
  394. });
  395. }
  396. // insert card
  397. const cardId = Cards.direct.insert(cardToCreate);
  398. // keep track of Wekan id => Wekan id
  399. this.cards[card._id] = cardId;
  400. // // log activity
  401. // Activities.direct.insert({
  402. // activityType: 'importCard',
  403. // boardId,
  404. // cardId,
  405. // createdAt: this._now(),
  406. // listId: cardToCreate.listId,
  407. // source: {
  408. // id: card._id,
  409. // system: 'Wekan',
  410. // },
  411. // // we attribute the import to current user,
  412. // // not the author of the original card
  413. // userId: this._user(),
  414. // });
  415. // add comments
  416. const comments = this.comments[card._id];
  417. if (comments) {
  418. comments.forEach(comment => {
  419. const commentToCreate = {
  420. boardId,
  421. cardId,
  422. createdAt: this._now(comment.createdAt),
  423. text: comment.text,
  424. // we attribute the comment to the original author, default to current user
  425. userId: this._user(comment.userId),
  426. };
  427. // dateLastActivity will be set from activity insert, no need to
  428. // update it ourselves
  429. const commentId = CardComments.direct.insert(commentToCreate);
  430. this.commentIds[comment._id] = commentId;
  431. // Activities.direct.insert({
  432. // activityType: 'addComment',
  433. // boardId: commentToCreate.boardId,
  434. // cardId: commentToCreate.cardId,
  435. // commentId,
  436. // createdAt: this._now(commentToCreate.createdAt),
  437. // // we attribute the addComment (not the import)
  438. // // to the original author - it is needed by some UI elements.
  439. // userId: commentToCreate.userId,
  440. // });
  441. });
  442. }
  443. const attachments = this.attachments[card._id];
  444. const wekanCoverId = card.coverId;
  445. if (attachments && Meteor.isServer) {
  446. attachments.forEach(att => {
  447. const self = this;
  448. const opts = {
  449. type: att.type ? att.type : undefined,
  450. userId: self._user(att.userId),
  451. meta: {
  452. boardId,
  453. cardId,
  454. source: 'import',
  455. },
  456. };
  457. const cb = (error, fileObj) => {
  458. if (error) {
  459. throw error;
  460. }
  461. self.attachmentIds[att._id] = fileObj._id;
  462. if (wekanCoverId === att._id) {
  463. Cards.direct.update(cardId, {
  464. $set: { coverId: fileObj._id },
  465. });
  466. }
  467. };
  468. if (att.url) {
  469. Attachments.load(att.url, opts, cb, true);
  470. } else if (att.file) {
  471. Attachments.insert(att.file, opts, cb, true);
  472. }
  473. });
  474. }
  475. result.push(cardId);
  476. });
  477. return result;
  478. }
  479. /**
  480. * Create the Wekan custom fields corresponding to the supplied Wekan
  481. * custom fields.
  482. * @param wekanCustomFields
  483. * @param boardId
  484. */
  485. createCustomFields(wekanCustomFields, boardId) {
  486. wekanCustomFields.forEach((field, fieldIndex) => {
  487. const fieldToCreate = {
  488. boardIds: [boardId],
  489. name: field.name,
  490. type: field.type,
  491. settings: field.settings,
  492. showOnCard: field.showOnCard,
  493. showLabelOnMiniCard: field.showLabelOnMiniCard,
  494. automaticallyOnCard: field.automaticallyOnCard,
  495. alwaysOnCard: field.alwaysOnCard,
  496. //use date "now" if now created at date is provided (e.g. for very old boards)
  497. createdAt: this._now(this.createdAt.customFields[field._id]),
  498. modifiedAt: field.modifiedAt,
  499. };
  500. //insert copy of custom field
  501. const fieldId = CustomFields.direct.insert(fieldToCreate);
  502. //set modified date to now
  503. CustomFields.direct.update(fieldId, {
  504. $set: {
  505. modifiedAt: this._now(),
  506. },
  507. });
  508. //store mapping of old id to new id
  509. this.customFields[field._id] = fieldId;
  510. });
  511. }
  512. // Create labels if they do not exist and load this.labels.
  513. createLabels(wekanLabels, board) {
  514. wekanLabels.forEach(label => {
  515. const color = label.color;
  516. const name = label.name;
  517. const existingLabel = board.getLabel(name, color);
  518. if (existingLabel) {
  519. this.labels[label.id] = existingLabel._id;
  520. } else {
  521. const idLabelCreated = board.pushLabel(name, color);
  522. this.labels[label.id] = idLabelCreated;
  523. }
  524. });
  525. }
  526. createLists(wekanLists, boardId) {
  527. wekanLists.forEach((list, listIndex) => {
  528. const listToCreate = {
  529. archived: list.archived,
  530. boardId,
  531. // We are being defensing here by providing a default date (now) if the
  532. // creation date wasn't found on the action log. This happen on old
  533. // Wekan boards (eg from 2013) that didn't log the 'createList' action
  534. // we require.
  535. createdAt: this._now(this.createdAt.lists[list.id]),
  536. title: list.title,
  537. sort: list.sort ? list.sort : listIndex,
  538. };
  539. const listId = Lists.direct.insert(listToCreate);
  540. Lists.direct.update(listId, {
  541. $set: {
  542. updatedAt: this._now(),
  543. },
  544. });
  545. this.lists[list._id] = listId;
  546. // // log activity
  547. // Activities.direct.insert({
  548. // activityType: 'importList',
  549. // boardId,
  550. // createdAt: this._now(),
  551. // listId,
  552. // source: {
  553. // id: list._id,
  554. // system: 'Wekan',
  555. // },
  556. // // We attribute the import to current user,
  557. // // not the creator of the original object
  558. // userId: this._user(),
  559. // });
  560. });
  561. }
  562. createSwimlanes(wekanSwimlanes, boardId) {
  563. wekanSwimlanes.forEach((swimlane, swimlaneIndex) => {
  564. const swimlaneToCreate = {
  565. archived: swimlane.archived,
  566. boardId,
  567. // We are being defensing here by providing a default date (now) if the
  568. // creation date wasn't found on the action log. This happen on old
  569. // Wekan boards (eg from 2013) that didn't log the 'createList' action
  570. // we require.
  571. createdAt: this._now(this.createdAt.swimlanes[swimlane._id]),
  572. title: swimlane.title,
  573. sort: swimlane.sort ? swimlane.sort : swimlaneIndex,
  574. };
  575. // set color
  576. if (swimlane.color) {
  577. swimlaneToCreate.color = swimlane.color;
  578. }
  579. const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate);
  580. Swimlanes.direct.update(swimlaneId, {
  581. $set: {
  582. updatedAt: this._now(),
  583. },
  584. });
  585. this.swimlanes[swimlane._id] = swimlaneId;
  586. });
  587. }
  588. createSubtasks(wekanCards) {
  589. wekanCards.forEach(card => {
  590. // get new id of card (in created / new board)
  591. const cardIdInNewBoard = this.cards[card._id];
  592. //If there is a mapped parent card, use the mapped card
  593. // this means, the card and parent were in the same source board
  594. //If there is no mapped parent card, use the original parent id,
  595. // this should handle cases where source and parent are in different boards
  596. // Note: This can only handle board cloning (within the same wekan instance).
  597. // When importing boards between instances the IDs are definitely
  598. // lost if source and parent are two different boards
  599. // This is not the place to fix it, the entire subtask system needs to be rethought there.
  600. const parentIdInNewBoard = this.cards[card.parentId]
  601. ? this.cards[card.parentId]
  602. : card.parentId;
  603. //if the parent card exists, proceed
  604. if (ReactiveCache.getCard(parentIdInNewBoard)) {
  605. //set parent id of the card in the new board to the new id of the parent
  606. Cards.direct.update(cardIdInNewBoard, {
  607. $set: {
  608. parentId: parentIdInNewBoard,
  609. },
  610. });
  611. }
  612. });
  613. }
  614. createChecklists(wekanChecklists) {
  615. const result = [];
  616. wekanChecklists.forEach((checklist, checklistIndex) => {
  617. // Create the checklist
  618. const checklistToCreate = {
  619. cardId: this.cards[checklist.cardId],
  620. title: checklist.title,
  621. createdAt: checklist.createdAt,
  622. sort: checklist.sort ? checklist.sort : checklistIndex,
  623. };
  624. const checklistId = Checklists.direct.insert(checklistToCreate);
  625. this.checklists[checklist._id] = checklistId;
  626. result.push(checklistId);
  627. });
  628. return result;
  629. }
  630. createTriggers(wekanTriggers, boardId) {
  631. wekanTriggers.forEach(trigger => {
  632. if (trigger.hasOwnProperty('labelId')) {
  633. trigger.labelId = this.labels[trigger.labelId];
  634. }
  635. if (trigger.hasOwnProperty('memberId')) {
  636. trigger.memberId = this.members[trigger.memberId];
  637. }
  638. trigger.boardId = boardId;
  639. const oldId = trigger._id;
  640. delete trigger._id;
  641. this.triggers[oldId] = Triggers.direct.insert(trigger);
  642. });
  643. }
  644. createActions(wekanActions, boardId) {
  645. wekanActions.forEach(action => {
  646. if (action.hasOwnProperty('labelId')) {
  647. action.labelId = this.labels[action.labelId];
  648. }
  649. if (action.hasOwnProperty('memberId')) {
  650. action.memberId = this.members[action.memberId];
  651. }
  652. action.boardId = boardId;
  653. const oldId = action._id;
  654. delete action._id;
  655. this.actions[oldId] = Actions.direct.insert(action);
  656. });
  657. }
  658. createRules(wekanRules, boardId) {
  659. wekanRules.forEach(rule => {
  660. // Create the rule
  661. rule.boardId = boardId;
  662. rule.triggerId = this.triggers[rule.triggerId];
  663. rule.actionId = this.actions[rule.actionId];
  664. delete rule._id;
  665. Rules.direct.insert(rule);
  666. });
  667. }
  668. createChecklistItems(wekanChecklistItems) {
  669. wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => {
  670. //Check if the checklist for this item (still) exists
  671. //If a checklist was deleted, but items remain, the import would error out here
  672. //Leading to no further checklist items being imported
  673. if (this.checklists[checklistitem.checklistId]) {
  674. // Create the checklistItem
  675. const checklistItemTocreate = {
  676. title: checklistitem.title,
  677. checklistId: this.checklists[checklistitem.checklistId],
  678. cardId: this.cards[checklistitem.cardId],
  679. sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex,
  680. isFinished: checklistitem.isFinished,
  681. };
  682. const checklistItemId = ChecklistItems.direct.insert(
  683. checklistItemTocreate,
  684. );
  685. this.checklistItems[checklistitem._id] = checklistItemId;
  686. }
  687. });
  688. }
  689. parseActivities(wekanBoard) {
  690. wekanBoard.activities.forEach(activity => {
  691. switch (activity.activityType) {
  692. case 'addAttachment': {
  693. // We have to be cautious, because the attachment could have been removed later.
  694. // In that case Wekan still reports its addition, but removes its 'url' field.
  695. // So we test for that
  696. const wekanAttachment = wekanBoard.attachments.filter(attachment => {
  697. return attachment._id === activity.attachmentId;
  698. })[0];
  699. if (typeof wekanAttachment !== 'undefined' && wekanAttachment) {
  700. if (wekanAttachment.url || wekanAttachment.file) {
  701. // we cannot actually create the Wekan attachment, because we don't yet
  702. // have the cards to attach it to, so we store it in the instance variable.
  703. const wekanCardId = activity.cardId;
  704. if (!this.attachments[wekanCardId]) {
  705. this.attachments[wekanCardId] = [];
  706. }
  707. this.attachments[wekanCardId].push(wekanAttachment);
  708. }
  709. }
  710. break;
  711. }
  712. case 'addComment': {
  713. const wekanComment = wekanBoard.comments.filter(comment => {
  714. return comment._id === activity.commentId;
  715. })[0];
  716. const id = activity.cardId;
  717. if (!this.comments[id]) {
  718. this.comments[id] = [];
  719. }
  720. this.comments[id].push(wekanComment);
  721. break;
  722. }
  723. case 'createBoard': {
  724. this.createdAt.board = activity.createdAt;
  725. break;
  726. }
  727. case 'createCard': {
  728. const cardId = activity.cardId;
  729. this.createdAt.cards[cardId] = activity.createdAt;
  730. this.createdBy.cards[cardId] = activity.userId;
  731. break;
  732. }
  733. case 'createList': {
  734. const listId = activity.listId;
  735. this.createdAt.lists[listId] = activity.createdAt;
  736. break;
  737. }
  738. case 'createSwimlane': {
  739. const swimlaneId = activity.swimlaneId;
  740. this.createdAt.swimlanes[swimlaneId] = activity.createdAt;
  741. break;
  742. }
  743. case 'createCustomField': {
  744. const customFieldId = activity.customFieldId;
  745. this.createdAt.customFields[customFieldId] = activity.createdAt;
  746. break;
  747. }
  748. }
  749. });
  750. }
  751. importActivities(activities, boardId) {
  752. activities.forEach(activity => {
  753. switch (activity.activityType) {
  754. // Board related activities
  755. // TODO: addBoardMember, removeBoardMember
  756. case 'createBoard': {
  757. Activities.direct.insert({
  758. userId: this._user(activity.userId),
  759. type: 'board',
  760. activityTypeId: boardId,
  761. activityType: activity.activityType,
  762. boardId,
  763. createdAt: this._now(activity.createdAt),
  764. });
  765. break;
  766. }
  767. // List related activities
  768. // TODO: removeList, archivedList
  769. case 'createList': {
  770. Activities.direct.insert({
  771. userId: this._user(activity.userId),
  772. type: 'list',
  773. activityType: activity.activityType,
  774. listId: this.lists[activity.listId],
  775. boardId,
  776. createdAt: this._now(activity.createdAt),
  777. });
  778. break;
  779. }
  780. // Card related activities
  781. // TODO: archivedCard, restoredCard, joinMember, unjoinMember
  782. case 'createCard': {
  783. Activities.direct.insert({
  784. userId: this._user(activity.userId),
  785. activityType: activity.activityType,
  786. listId: this.lists[activity.listId],
  787. cardId: this.cards[activity.cardId],
  788. boardId,
  789. createdAt: this._now(activity.createdAt),
  790. });
  791. break;
  792. }
  793. case 'moveCard': {
  794. Activities.direct.insert({
  795. userId: this._user(activity.userId),
  796. oldListId: this.lists[activity.oldListId],
  797. activityType: activity.activityType,
  798. listId: this.lists[activity.listId],
  799. cardId: this.cards[activity.cardId],
  800. boardId,
  801. createdAt: this._now(activity.createdAt),
  802. });
  803. break;
  804. }
  805. // Comment related activities
  806. case 'addComment': {
  807. Activities.direct.insert({
  808. userId: this._user(activity.userId),
  809. activityType: activity.activityType,
  810. cardId: this.cards[activity.cardId],
  811. commentId: this.commentIds[activity.commentId],
  812. boardId,
  813. createdAt: this._now(activity.createdAt),
  814. });
  815. break;
  816. }
  817. // Attachment related activities
  818. case 'addAttachment': {
  819. Activities.direct.insert({
  820. userId: this._user(activity.userId),
  821. type: 'card',
  822. activityType: activity.activityType,
  823. attachmentId: this.attachmentIds[activity.attachmentId],
  824. cardId: this.cards[activity.cardId],
  825. boardId,
  826. createdAt: this._now(activity.createdAt),
  827. });
  828. break;
  829. }
  830. // Checklist related activities
  831. case 'addChecklist': {
  832. Activities.direct.insert({
  833. userId: this._user(activity.userId),
  834. activityType: activity.activityType,
  835. cardId: this.cards[activity.cardId],
  836. checklistId: this.checklists[activity.checklistId],
  837. boardId,
  838. createdAt: this._now(activity.createdAt),
  839. });
  840. break;
  841. }
  842. case 'addChecklistItem': {
  843. Activities.direct.insert({
  844. userId: this._user(activity.userId),
  845. activityType: activity.activityType,
  846. cardId: this.cards[activity.cardId],
  847. checklistId: this.checklists[activity.checklistId],
  848. checklistItemId: activity.checklistItemId.replace(
  849. activity.checklistId,
  850. this.checklists[activity.checklistId],
  851. ),
  852. boardId,
  853. createdAt: this._now(activity.createdAt),
  854. });
  855. break;
  856. }
  857. }
  858. });
  859. }
  860. //check(board) {
  861. check() {
  862. //try {
  863. // check(data, {
  864. // membersMapping: Match.Optional(Object),
  865. // });
  866. // this.checkActivities(board.activities);
  867. // this.checkBoard(board);
  868. // this.checkLabels(board.labels);
  869. // this.checkLists(board.lists);
  870. // this.checkSwimlanes(board.swimlanes);
  871. // this.checkCards(board.cards);
  872. //this.checkChecklists(board.checklists);
  873. // this.checkRules(board.rules);
  874. // this.checkActions(board.actions);
  875. //this.checkTriggers(board.triggers);
  876. //this.checkChecklistItems(board.checklistItems);
  877. //} catch (e) {
  878. // throw new Meteor.Error('error-json-schema');
  879. // }
  880. }
  881. create(board, currentBoardId) {
  882. // TODO : Make isSandstorm variable global
  883. const isSandstorm =
  884. Meteor.settings &&
  885. Meteor.settings.public &&
  886. Meteor.settings.public.sandstorm;
  887. if (isSandstorm && currentBoardId) {
  888. const currentBoard = ReactiveCache.getBoard(currentBoardId);
  889. currentBoard.archive();
  890. }
  891. this.parseActivities(board);
  892. const boardId = this.createBoardAndLabels(board);
  893. this.createLists(board.lists, boardId);
  894. this.createSwimlanes(board.swimlanes, boardId);
  895. this.createCustomFields(board.customFields, boardId);
  896. this.createCards(board.cards, boardId);
  897. this.createSubtasks(board.cards);
  898. this.createChecklists(board.checklists);
  899. this.createChecklistItems(board.checklistItems);
  900. this.importActivities(board.activities, boardId);
  901. this.createTriggers(board.triggers, boardId);
  902. this.createActions(board.actions, boardId);
  903. this.createRules(board.rules, boardId);
  904. // XXX add members
  905. return boardId;
  906. }
  907. }