소스 검색

Merge branch 'xavierpriour-devel' into devel

Conflicts:
	models/import.js
Maxime Quandalle 9 년 전
부모
커밋
7a5f030cc8

+ 31 - 25
client/components/activities/activities.jade

@@ -14,41 +14,56 @@ template(name="boardActivities")
       p.activity-desc
         +memberName(user=user)
 
-        if($eq activityType 'createBoard')
-          | {{_ 'activity-created' boardLabel}}.
+        if($eq activityType 'addAttachment')
+          | {{{_ 'activity-attached' attachmentLink cardLink}}}.
 
-        if($eq activityType 'createList')
-          | {{_ 'activity-added' list.title boardLabel}}.
+        if($eq activityType 'addBoardMember')
+          | {{{_ 'activity-added' memberLink boardLabel}}}.
+
+        if($eq activityType 'addComment')
+          | {{{_ 'activity-on' cardLink}}}
+          a.activity-comment(href="{{ card.absoluteUrl }}")
+            +viewer
+              = comment.text
+
+        if($eq activityType 'archivedCard')
+          | {{{_ 'activity-archived' cardLink}}}.
 
         if($eq activityType 'archivedList')
           | {{_ 'activity-archived' list.title}}.
 
+        if($eq activityType 'createBoard')
+          | {{_ 'activity-created' boardLabel}}.
+
         if($eq activityType 'createCard')
           | {{{_ 'activity-added' cardLink boardLabel}}}.
 
+        if($eq activityType 'createList')
+          | {{_ 'activity-added' list.title boardLabel}}.
+
+        if($eq activityType 'importBoard')
+          | {{{_ 'activity-imported-board' boardLabel sourceLink}}}.
+
         if($eq activityType 'importCard')
           | {{{_ 'activity-imported' cardLink boardLabel sourceLink}}}.
 
-        if($eq activityType 'archivedCard')
-          | {{{_ 'activity-archived' cardLink}}}.
+        if($eq activityType 'importList')
+          | {{{_ 'activity-imported' listLabel boardLabel sourceLink}}}.
 
-        if($eq activityType 'restoredCard')
-          | {{{_ 'activity-sent' cardLink boardLabel}}}.
+        if($eq activityType 'joinMember')
+          if($eq currentUser._id member._id)
+            | {{{_ 'activity-joined' cardLink}}}.
+          else
+            | {{{_ 'activity-added' memberLink cardLink}}}.
 
         if($eq activityType 'moveCard')
           | {{{_ 'activity-moved' cardLink oldList.title list.title}}}.
 
-        if($eq activityType 'addBoardMember')
-          | {{{_ 'activity-added' memberLink boardLabel}}}.
-
         if($eq activityType 'removeBoardMember')
           | {{{_ 'activity-excluded' memberLink boardLabel}}}.
 
-        if($eq activityType 'joinMember')
-          if($eq currentUser._id member._id)
-            | {{{_ 'activity-joined' cardLink}}}.
-          else
-            | {{{_ 'activity-added' memberLink cardLink}}}.
+        if($eq activityType 'restoredCard')
+          | {{{_ 'activity-sent' cardLink boardLabel}}}.
 
         if($eq activityType 'unjoinMember')
           if($eq currentUser._id member._id)
@@ -56,15 +71,6 @@ template(name="boardActivities")
           else
             | {{{_ 'activity-removed' memberLink cardLink}}}.
 
-        if($eq activityType 'addComment')
-          | {{{_ 'activity-on' cardLink}}}
-          a.activity-comment(href="{{ card.absoluteUrl }}")
-            +viewer
-              = comment.text
-
-        if($eq activityType 'addAttachment')
-          | {{{_ 'activity-attached' attachmentLink cardLink}}}.
-
         span.activity-meta {{ moment createdAt }}
 
 template(name="cardActivities")

+ 14 - 3
client/components/activities/activities.js

@@ -60,11 +60,22 @@ BlazeComponent.extendComponent({
     }, card.title));
   },
 
+  listLabel() {
+    return this.currentData().list().title;
+  },
+
   sourceLink() {
     const source = this.currentData().source;
-    return source && Blaze.toHTML(HTML.A({
-      href: source.url,
-    }, source.system));
+    if(source) {
+      if(source.url) {
+        return Blaze.toHTML(HTML.A({
+          href: source.url,
+        }, source.system));
+      } else {
+        return source.system;
+      }
+    }
+    return null;
   },
 
   memberLink() {

+ 3 - 0
client/components/boards/boardHeader.jade

@@ -107,6 +107,9 @@ template(name="createBoardPopup")
           | {{{_ 'board-private-info'}}}
         a.js-change-visibility {{_ 'change'}}.
     input.primary.wide(type="submit" value="{{_ 'create'}}")
+    span.quiet
+      | {{_ 'or'}}
+      a.js-import {{_ 'import-board'}}
 
 
 template(name="boardChangeTitlePopup")

+ 1 - 0
client/components/boards/boardHeader.js

@@ -145,6 +145,7 @@ BlazeComponent.extendComponent({
         this.setVisibility(this.currentData());
       },
       'click .js-change-visibility': this.toggleVisibilityMenu,
+      'click .js-import': Popup.open('boardImportBoard'),
       submit: this.onSubmit,
     }];
   },

+ 2 - 0
client/components/boards/boardHeader.styl

@@ -0,0 +1,2 @@
+a.js-import
+  text-decoration underline

+ 7 - 0
client/components/import/import.jade

@@ -0,0 +1,7 @@
+template(name="importPopup")
+  if error.get
+    .warning {{_ error.get}}
+  form
+    p: label(for='import-textarea') {{_ getLabel}}
+    textarea#import-textarea.js-import-json(placeholder="{{_ 'import-json-placeholder'}}" autofocus)
+    input.primary.wide(type="submit" value="{{_ 'import'}}")

+ 90 - 0
client/components/import/import.js

@@ -0,0 +1,90 @@
+/// Abstract root for all import popup screens.
+/// Descendants must define:
+/// - getMethodName(): return the Meteor method to call for import, passing json
+/// data decoded as object and additional data (see below);
+/// - getAdditionalData(): return object containing additional data passed to
+/// Meteor method (like list ID and position for a card import);
+/// - getLabel(): i18n key for the text displayed in the popup, usually to
+/// explain how to get the data out of the source system.
+const ImportPopup = BlazeComponent.extendComponent({
+  template() {
+    return 'importPopup';
+  },
+
+  events() {
+    return [{
+      'submit': (evt) => {
+        evt.preventDefault();
+        const dataJson = $(evt.currentTarget).find('.js-import-json').val();
+        let dataObject;
+        try {
+          dataObject = JSON.parse(dataJson);
+        } catch (e) {
+          this.setError('error-json-malformed');
+          return;
+        }
+        Meteor.call(this.getMethodName(), dataObject, this.getAdditionalData(),
+          (error, response) => {
+            if (error) {
+              this.setError(error.error);
+            } else {
+              Filter.addException(response);
+              this.onFinish(response);
+            }
+          }
+        );
+      },
+    }];
+  },
+
+  onCreated() {
+    this.error = new ReactiveVar('');
+  },
+
+  setError(error) {
+    this.error.set(error);
+  },
+
+  onFinish() {
+    Popup.close();
+  },
+});
+
+ImportPopup.extendComponent({
+  getAdditionalData() {
+    const listId = this.data()._id;
+    const selector = `#js-list-${this.currentData()._id} .js-minicard:first`;
+    const firstCardDom = $(selector).get(0);
+    const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
+    const result = {listId, sortIndex};
+    return result;
+  },
+
+  getMethodName() {
+    return 'importTrelloCard';
+  },
+
+  getLabel() {
+    return 'import-card-trello-instruction';
+  },
+}).register('listImportCardPopup');
+
+ImportPopup.extendComponent({
+  getAdditionalData() {
+    const result = {};
+    return result;
+  },
+
+  getMethodName() {
+    return 'importTrelloBoard';
+  },
+
+  getLabel() {
+    return 'import-board-trello-instruction';
+  },
+
+  onFinish(response) {
+    Utils.goBoardId(response);
+  },
+}).register('boardImportBoardPopup');
+

+ 0 - 9
client/components/lists/listHeader.jade

@@ -31,15 +31,6 @@ template(name="listActionPopup")
 template(name="listMoveCardsPopup")
   +boardLists
 
-template(name="listImportCardPopup")
-  if error.get
-    .warning {{_ error.get}}
-  form
-    label
-      | {{_ 'card-json'}}
-      textarea.js-card-json(placeholder="{{_ 'card-json-placeholder'}}" autofocus)
-    input.primary.wide(type="submit" value="{{_ 'import'}}")
-
 template(name="boardLists")
   ul.pop-over-list
     each currentBoard.lists

+ 0 - 39
client/components/lists/listHeader.js

@@ -49,45 +49,6 @@ Template.listActionPopup.events({
   },
 });
 
-
-BlazeComponent.extendComponent({
-  events() {
-    return [{
-      'submit': (evt) => {
-        evt.preventDefault();
-        const jsonData = $(evt.currentTarget).find('textarea').val();
-        const firstCardDom = $(`#js-list-${this.currentData()._id} .js-minicard:first`).get(0);
-        const sortIndex = Utils.calculateIndex(null, firstCardDom).base;
-        let trelloCard;
-        try {
-          trelloCard = JSON.parse(jsonData);
-        } catch (e) {
-          this.setError('error-json-malformed');
-          return;
-        }
-        Meteor.call('importTrelloCard', trelloCard, this.currentData()._id, sortIndex,
-          (error, response) => {
-            if (error) {
-              this.setError(error.error);
-            } else {
-              Filter.addException(response);
-              Popup.close();
-            }
-          }
-        );
-      },
-    }];
-  },
-
-  onCreated() {
-    this.error = new ReactiveVar('');
-  },
-
-  setError(error) {
-    this.error.set(error);
-  },
-}).register('listImportCardPopup');
-
 Template.listMoveCardsPopup.events({
   'click .js-select-list'() {
     const fromList = Template.parentData(2).data;

+ 3 - 3
client/components/main/popup.styl

@@ -17,9 +17,11 @@ $popupWidth = 300px
     margin: 4px -10px
     width: $popupWidth
 
+  p,
+  textarea,
   input[type="text"],
   input[type="email"],
-  input[type="password"]
+  input[type="password"],
   input[type="file"]
     margin: 4px 0 12px
     width: 100%
@@ -30,8 +32,6 @@ $popupWidth = 300px
 
   textarea
     height: 72px
-    margin: 4px 0 12px
-    width: 100%
 
   .header
     height: 36px

+ 7 - 2
i18n/en.i18n.json

@@ -8,6 +8,7 @@
     "activity-created": "created %s",
     "activity-excluded": "excluded %s from %s",
     "activity-imported": "imported %s into %s from %s",
+    "activity-imported-board": "imported %s from %s",
     "activity-joined": "joined %s",
     "activity-moved": "moved %s from %s to %s",
     "activity-on": "on %s",
@@ -54,6 +55,7 @@
     "boardChangeColorPopup-title": "Change Board Background",
     "boardChangeTitlePopup-title": "Rename Board",
     "boardChangeVisibilityPopup-title": "Change Visibility",
+    "boardImportBoardPopup-title": "Import board from Trello",
     "boardMenuPopup-title": "Board Menu",
     "boards": "Boards",
     "bucket-example": "Like “Bucket List” for example",
@@ -66,8 +68,6 @@
     "card-edit-attachments": "Edit attachments",
     "card-edit-labels": "Edit labels",
     "card-edit-members": "Edit members",
-    "card-json": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
-    "card-json-placeholder": "Paste your valid JSON data here",
     "card-labels-title": "Change the labels for the card.",
     "card-members-title": "Add or remove members of the board from the card.",
     "cardAttachmentsPopup-title": "Attach From",
@@ -136,7 +136,11 @@
     "header-logo-title": "Go back to your boards page.",
     "home": "Home",
     "import": "Import",
+    "import-board": "import from Trello",
+    "import-board-trello-instruction": "In your Trello board, go to 'Menu', then 'More', 'Print and Export', 'Export JSON', and copy the resulting text",
     "import-card": "Import a Trello card",
+    "import-card-trello-instruction": "Go to a Trello card, select 'Share and more...' then 'Export JSON' and copy the resulting text",
+    "import-json-placeholder": "Paste your valid JSON data here",
     "info": "Infos",
     "initials": "Initials",
     "joined": "joined",
@@ -175,6 +179,7 @@
     "normal": "Normal",
     "normal-desc": "Can view and edit cards. Can't change settings.",
     "optional": "optional",
+    "or": "or",
     "page-maybe-private": "This page may be private. You may be able to view it by <a href='%s'>logging in</a>.",
     "page-not-found": "Page not found.",
     "password": "Password",

+ 8 - 0
models/boards.js

@@ -111,6 +111,14 @@ Boards.helpers({
   colorClass() {
     return `board-color-${this.color}`;
   },
+
+  // XXX currently mutations return no value so we have an issue when using addLabel in import
+  // XXX waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
+  pushLabel(name, color) {
+    const _id = Random.id(6);
+    Boards.direct.update(this._id, { $push: {labels: { _id, name, color }}});
+    return _id;
+  },
 });
 
 Boards.mutations({

+ 345 - 103
models/import.js

@@ -1,36 +1,349 @@
-Meteor.methods({
-  importTrelloCard(trelloCard, listId, sortIndex) {
-    // 1. check parameters are ok from a syntax point of view
-    const DateString = Match.Where(function (dateAsString) {
-      check(dateAsString, String);
-      return moment(dateAsString, moment.ISO_8601).isValid();
+const DateString = Match.Where(function (dateAsString) {
+  check(dateAsString, String);
+  return moment(dateAsString, moment.ISO_8601).isValid();
+});
+
+class TrelloCreator {
+  constructor() {
+    // The object creation dates, indexed by Trello id (so we only parse actions
+    // once!)
+    this.createdAt = {
+      board: null,
+      cards: {},
+      lists: {},
+    };
+    // Map of labels Trello ID => Wekan ID
+    this.labels = {};
+    // Map of lists Trello ID => Wekan ID
+    this.lists = {};
+    // The comments, indexed by Trello card id (to map when importing cards)
+    this.comments = {};
+  }
+
+  checkActions(trelloActions) {
+    check(trelloActions, [Match.ObjectIncluding({
+      data: Object,
+      date: DateString,
+      type: String,
+    })]);
+    // XXX we could perform more thorough checks based on action type
+  }
+
+  checkBoard(trelloBoard) {
+    check(trelloBoard, Match.ObjectIncluding({
+      closed: Boolean,
+      name: String,
+      prefs: Match.ObjectIncluding({
+        // XXX refine control by validating 'background' against a list of
+        // allowed values (is it worth the maintenance?)
+        background: String,
+        permissionLevel: Match.Where((value) => {
+          return ['org', 'private', 'public'].indexOf(value)>= 0;
+        }),
+      }),
+    }));
+  }
+
+  checkCards(trelloCards) {
+    check(trelloCards, [Match.ObjectIncluding({
+      closed: Boolean,
+      dateLastActivity: DateString,
+      desc: String,
+      idLabels: [String],
+      idMembers: [String],
+      name: String,
+      pos: Number,
+    })]);
+  }
+
+  checkLabels(trelloLabels) {
+    check(trelloLabels, [Match.ObjectIncluding({
+      // XXX refine control by validating 'color' against a list of allowed
+      // values (is it worth the maintenance?)
+      color: String,
+      name: String,
+    })]);
+  }
+
+  checkLists(trelloLists) {
+    check(trelloLists, [Match.ObjectIncluding({
+      closed: Boolean,
+      name: String,
+    })]);
+  }
+
+  // You must call parseActions before calling this one.
+  createBoardAndLabels(trelloBoard) {
+    const createdAt = this.createdAt.board;
+    const boardToCreate = {
+      archived: trelloBoard.closed,
+      color: this.getColor(trelloBoard.prefs.background),
+      createdAt,
+      labels: [],
+      members: [{
+        userId: Meteor.userId(),
+        isAdmin: true,
+        isActive: true,
+      }],
+      permission: this.getPermission(trelloBoard.prefs.permissionLevel),
+      slug: getSlug(trelloBoard.name) || 'board',
+      stars: 0,
+      title: trelloBoard.name,
+    };
+    trelloBoard.labels.forEach((label) => {
+      const labelToCreate = {
+        _id: Random.id(6),
+        color: label.color,
+        name: label.name,
+      };
+      // We need to remember them by Trello ID, as this is the only ref we have
+      // when importing cards.
+      this.labels[label.id] = labelToCreate._id;
+      boardToCreate.labels.push(labelToCreate);
+    });
+    const now = new Date();
+    const boardId = Boards.direct.insert(boardToCreate);
+    Boards.direct.update(boardId, {$set: {modifiedAt: now}});
+    // log activity
+    Activities.direct.insert({
+      activityType: 'importBoard',
+      boardId,
+      createdAt: now,
+      source: {
+        id: trelloBoard.id,
+        system: 'Trello',
+        url: trelloBoard.url,
+      },
+      // We attribute the import to current user, not the one from the original
+      // object.
+      userId: Meteor.userId(),
+    });
+    return boardId;
+  }
+
+  // Create labels if they do not exist and load this.labels.
+  createLabels(trelloLabels, board) {
+    trelloLabels.forEach((label) => {
+      const color = label.color;
+      const name = label.name;
+      const existingLabel = board.getLabel(name, color);
+      if (existingLabel) {
+        this.labels[label.id] = existingLabel._id;
+      } else {
+        const idLabelCreated = board.pushLabel(name, color);
+        this.labels[label.id] = idLabelCreated;
+      }
+    });
+  }
+
+  createLists(trelloLists, boardId) {
+    trelloLists.forEach((list) => {
+      const listToCreate = {
+        archived: list.closed,
+        boardId,
+        // We are being defensing here by providing a default date (now) if the
+        // creation date wasn't found on the action log. This happen on old
+        // Trello boards (eg from 2013) that didn't log the 'createList' action
+        // we require.
+        createdAt: new Date(this.createdAt.lists[list.id] || Date.now()),
+        title: list.name,
+        userId: Meteor.userId(),
+      };
+      const listId = Lists.direct.insert(listToCreate);
+      const now = new Date();
+      Lists.direct.update(listId, {$set: {'updatedAt': now}});
+      this.lists[list.id] = listId;
+      // log activity
+      Activities.direct.insert({
+        activityType: 'importList',
+        boardId,
+        createdAt: now,
+        listId,
+        source: {
+          id: list.id,
+          system: 'Trello',
+        },
+        // We attribute the import to current user, not the one from the
+        // original object
+        userId: Meteor.userId(),
+      });
+    });
+  }
+
+  createCardsAndComments(trelloCards, boardId) {
+    const result = [];
+    trelloCards.forEach((card) => {
+      const cardToCreate = {
+        archived: card.closed,
+        boardId,
+        createdAt: new Date(this.createdAt.cards[card.id]  || Date.now()),
+        dateLastActivity: new Date(),
+        description: card.desc,
+        listId: this.lists[card.idList],
+        sort: card.pos,
+        title: card.name,
+        // XXX use the original user?
+        userId: Meteor.userId(),
+      };
+      // add labels
+      if (card.idLabels) {
+        cardToCreate.labelIds = card.idLabels.map((trelloId) => {
+          return this.labels[trelloId];
+        });
+      }
+      // insert card
+      const cardId = Cards.direct.insert(cardToCreate);
+      // log activity
+      Activities.direct.insert({
+        activityType: 'importCard',
+        boardId,
+        cardId,
+        createdAt: new Date(),
+        listId: cardToCreate.listId,
+        source: {
+          id: card.id,
+          system: 'Trello',
+          url: card.url,
+        },
+        // we attribute the import to current user, not the one from the
+        // original card
+        userId: Meteor.userId(),
+      });
+      // add comments
+      const comments = this.comments[card.id];
+      if (comments) {
+        comments.forEach((comment) => {
+          const commentToCreate = {
+            boardId,
+            cardId,
+            createdAt: comment.date,
+            text: comment.data.text,
+            // XXX use the original comment user instead
+            userId: Meteor.userId(),
+          };
+          // dateLastActivity will be set from activity insert, no need to
+          // update it ourselves
+          const commentId = CardComments.direct.insert(commentToCreate);
+          Activities.direct.insert({
+            activityType: 'addComment',
+            boardId: commentToCreate.boardId,
+            cardId: commentToCreate.cardId,
+            commentId,
+            createdAt: commentToCreate.createdAt,
+            userId: commentToCreate.userId,
+          });
+        });
+      }
+      // XXX add attachments
+      result.push(cardId);
+    });
+    return result;
+  }
+
+  getColor(trelloColorCode) {
+    // trello color name => wekan color
+    const mapColors = {
+      'blue': 'belize',
+      'orange': 'pumpkin',
+      'green': 'nephritis',
+      'red': 'pomegranate',
+      'purple': 'wisteria',
+      'pink': 'pomegranate',
+      'lime': 'nephritis',
+      'sky': 'belize',
+      'grey': 'midnight',
+    };
+    const wekanColor = mapColors[trelloColorCode];
+    return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0];
+  }
+
+  getPermission(trelloPermissionCode) {
+    if (trelloPermissionCode === 'public') {
+      return 'public';
+    }
+    // Wekan does NOT have organization level, so we default both 'private' and
+    // 'org' to private.
+    return 'private';
+  }
+
+  parseActions(trelloActions) {
+    trelloActions.forEach((action) => {
+      switch (action.type) {
+      case 'createBoard':
+        this.createdAt.board = action.date;
+        break;
+      case 'createCard':
+        const cardId = action.data.card.id;
+        this.createdAt.cards[cardId] = action.date;
+        break;
+      case 'createList':
+        const listId = action.data.list.id;
+        this.createdAt.lists[listId] = action.date;
+        break;
+      case 'commentCard':
+        const id = action.data.card.id;
+        if (this.comments[id]) {
+          this.comments[id].push(action);
+        } else {
+          this.comments[id] = [action];
+        }
+        break;
+      default:
+        // do nothing
+        break;
+      }
     });
+  }
+}
+
+Meteor.methods({
+  importTrelloBoard(trelloBoard, data) {
+    const trelloCreator = new TrelloCreator();
+
+    // 1. check all parameters are ok from a syntax point of view
     try {
-      check(trelloCard, Match.ObjectIncluding({
-        name: String,
-        desc: String,
-        closed: Boolean,
-        dateLastActivity: DateString,
-        labels: [Match.ObjectIncluding({
-          name: String,
-          color: String,
-        })],
-        actions: [Match.ObjectIncluding({
-          type: String,
-          date: DateString,
-          data: Object,
-        })],
-        members: [Object],
-      }));
-      check(listId, String);
-      check(sortIndex, Number);
+      // we don't use additional data - this should be an empty object
+      check(data, {});
+      trelloCreator.checkActions(trelloBoard.actions);
+      trelloCreator.checkBoard(trelloBoard);
+      trelloCreator.checkLabels(trelloBoard.labels);
+      trelloCreator.checkLists(trelloBoard.lists);
+      trelloCreator.checkCards(trelloBoard.cards);
     } catch (e) {
       throw new Meteor.Error('error-json-schema');
     }
 
+    // 2. check parameters are ok from a business point of view (exist &
+    // authorized) nothing to check, everyone can import boards in their account
+
+    // 3. create all elements
+    trelloCreator.parseActions(trelloBoard.actions);
+    const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
+    trelloCreator.createLists(trelloBoard.lists, boardId);
+    trelloCreator.createCardsAndComments(trelloBoard.cards, boardId);
+    // XXX add members
+    return boardId;
+  },
+
+  importTrelloCard(trelloCard, data) {
+    const trelloCreator = new TrelloCreator();
+
+    // 1. check parameters are ok from a syntax point of view
+    try {
+      check(data, {
+        listId: String,
+        sortIndex: Number,
+      });
+      trelloCreator.checkCards([trelloCard]);
+      trelloCreator.checkLabels(trelloCard.labels);
+      trelloCreator.checkActions(trelloCard.actions);
+    } catch(e) {
+      throw new Meteor.Error('error-json-schema');
+    }
+
     // 2. check parameters are ok from a business point of view (exist &
     // authorized)
-    const list = Lists.findOne(listId);
+    const list = Lists.findOne(data.listId);
     if (!list) {
       throw new Meteor.Error('error-list-doesNotExist');
     }
@@ -40,83 +353,12 @@ Meteor.methods({
       }
     }
 
-    // 3. map all fields for the card to create
-    const dateOfImport = new Date();
-    const cardToCreate = {
-      archived: trelloCard.closed,
-      boardId: list.boardId,
-      // this is a default date, we'll fetch the actual one from the actions array
-      createdAt: dateOfImport,
-      dateLastActivity: dateOfImport,
-      description: trelloCard.desc,
-      labelIds: [],
-      listId: list._id,
-      sort: sortIndex,
-      title: trelloCard.name,
-      // XXX use the original user?
-      userId: Meteor.userId(),
-    };
-
-    // 4. find actual creation date
-    const creationAction = trelloCard.actions.find((action) => {
-      return action.type === 'createCard';
-    });
-    if (creationAction) {
-      cardToCreate.createdAt = creationAction.date;
-    }
-
-    // 5. map labels - create missing ones
-    trelloCard.labels.forEach((currentLabel) => {
-      const { name, color } = currentLabel;
-      // `addLabel` won't create dublicate labels (ie labels with the same name
-      // and color) so here it is used more in a "enforceLabelExistence" way.
-      list.board().addLabel(name, color);
-      const { _id: labelId } = list.board().getLabel(name, color);
-      if (labelId) {
-        cardToCreate.labelIds.push(labelId);
-      }
-    });
-
-    // 6. insert new card into list
-    const cardId = Cards.direct.insert(cardToCreate);
-    Activities.direct.insert({
-      activityType: 'importCard',
-      boardId: cardToCreate.boardId,
-      cardId,
-      createdAt: dateOfImport,
-      listId: cardToCreate.listId,
-      source: {
-        id: trelloCard.id,
-        system: 'Trello',
-        url: trelloCard.url,
-      },
-      // we attribute the import to current user, not the one from the original
-      // card
-      userId: Meteor.userId(),
-    });
-
-    // 7. parse actions and add comments
-    trelloCard.actions.forEach((currentAction) => {
-      if (currentAction.type === 'commentCard') {
-        const commentToCreate = {
-          boardId: list.boardId,
-          cardId,
-          createdAt: currentAction.date,
-          text: currentAction.data.text,
-          // XXX use the original comment user instead
-          userId: Meteor.userId(),
-        };
-        const commentId = CardComments.direct.insert(commentToCreate);
-        Activities.direct.insert({
-          activityType: 'addComment',
-          boardId: commentToCreate.boardId,
-          cardId: commentToCreate.cardId,
-          commentId,
-          createdAt: commentToCreate.createdAt,
-          userId: commentToCreate.userId,
-        });
-      }
-    });
-    return cardId;
+    // 3. create all elements
+    trelloCreator.lists[trelloCard.idList] = data.listId;
+    trelloCreator.parseActions(trelloCard.actions);
+    const board = list.board();
+    trelloCreator.createLabels(trelloCard.labels, board);
+    const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id);
+    return cardIds[0];
   },
 });