Import board: map team permission, and refactor code to share with card import

This commit is contained in:
Xavier Priour 2015-10-19 20:14:29 +02:00
parent ec304de811
commit 8e0ad91191
2 changed files with 83 additions and 135 deletions

View file

@ -111,6 +111,14 @@ Boards.helpers({
colorClass() { colorClass() {
return `board-color-${this.color}`; 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({ Boards.mutations({

View file

@ -11,9 +11,9 @@ class TrelloCreator {
cards: {}, cards: {},
lists: {}, lists: {},
}; };
// the labels we created, indexed by Trello id (to map when importing cards) // map of labels Trello ID => Wekan ID
this.labels = {}; this.labels = {};
// the lists we created, indexed by Trello id (to map when importing cards) // map of lists Trello ID => Wekan ID
this.lists = {}; this.lists = {};
// the comments, indexed by Trello card id (to map when importing cards) // the comments, indexed by Trello card id (to map when importing cards)
this.comments = {}; this.comments = {};
@ -25,27 +25,41 @@ class TrelloCreator {
date: DateString, date: DateString,
type: String, type: String,
})]); })]);
// XXX perform deeper checks based on type // XXX we could perform more thorough checks based on action type
} }
checkBoard(trelloBoard) { checkBoard(trelloBoard) {
check(trelloBoard, Match.ObjectIncluding({ check(trelloBoard, Match.ObjectIncluding({
closed: Boolean, closed: Boolean,
labels: [Match.ObjectIncluding({
// XXX check versus list
color: String,
name: String,
})],
name: String, name: String,
prefs: Match.ObjectIncluding({ prefs: Match.ObjectIncluding({
// XXX check versus list // XXX refine control by validating 'background' against a list of allowed values (is it worth the maintenance?)
background: String, background: String,
// XXX check versus list permissionLevel: Match.Where((value) => {return ['org', 'private', 'public'].indexOf(value)>= 0;}),
permissionLevel: String,
}), }),
})); }));
} }
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) { checkLists(trelloLists) {
check(trelloLists, [Match.ObjectIncluding({ check(trelloLists, [Match.ObjectIncluding({
closed: Boolean, closed: Boolean,
@ -53,16 +67,6 @@ class TrelloCreator {
})]); })]);
} }
checkCards(trelloCards) {
check(trelloCards, [Match.ObjectIncluding({
closed: Boolean,
desc: String,
// XXX check idLabels
name: String,
pos: Number,
})]);
}
/** /**
* must call parseActions before calling this one * must call parseActions before calling this one
*/ */
@ -78,8 +82,7 @@ class TrelloCreator {
isAdmin: true, isAdmin: true,
isActive: true, isActive: true,
}], }],
// current mapping is easy as trello and wekan use same keys: 'private' and 'public' permission: this.getPermission(trelloBoard.prefs.permissionLevel),
permission: trelloBoard.prefs.permissionLevel,
slug: getSlug(trelloBoard.name) || 'board', slug: getSlug(trelloBoard.name) || 'board',
stars: 0, stars: 0,
title: trelloBoard.name, title: trelloBoard.name,
@ -91,7 +94,7 @@ class TrelloCreator {
name: label.name, name: label.name,
}; };
// we need to remember them by Trello ID, as this is the only ref we have when importing cards // we need to remember them by Trello ID, as this is the only ref we have when importing cards
this.labels[label.id] = labelToCreate; this.labels[label.id] = labelToCreate._id;
boardToCreate.labels.push(labelToCreate); boardToCreate.labels.push(labelToCreate);
}); });
const now = new Date(); const now = new Date();
@ -113,6 +116,23 @@ class TrelloCreator {
return boardId; 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) { createLists(trelloLists, boardId) {
trelloLists.forEach((list) => { trelloLists.forEach((list) => {
const listToCreate = { const listToCreate = {
@ -125,8 +145,7 @@ class TrelloCreator {
const listId = Lists.direct.insert(listToCreate); const listId = Lists.direct.insert(listToCreate);
const now = new Date(); const now = new Date();
Lists.direct.update(listId, {$set: {'updatedAt': now}}); Lists.direct.update(listId, {$set: {'updatedAt': now}});
listToCreate._id = listId; this.lists[list.id] = listId;
this.lists[list.id] = listToCreate;
// log activity // log activity
Activities.direct.insert({ Activities.direct.insert({
activityType: 'importList', activityType: 'importList',
@ -144,6 +163,7 @@ class TrelloCreator {
} }
createCardsAndComments(trelloCards, boardId) { createCardsAndComments(trelloCards, boardId) {
const result = [];
trelloCards.forEach((card) => { trelloCards.forEach((card) => {
const cardToCreate = { const cardToCreate = {
archived: card.closed, archived: card.closed,
@ -151,7 +171,7 @@ class TrelloCreator {
createdAt: this.createdAt.cards[card.id], createdAt: this.createdAt.cards[card.id],
dateLastActivity: new Date(), dateLastActivity: new Date(),
description: card.desc, description: card.desc,
listId: this.lists[card.idList]._id, listId: this.lists[card.idList],
sort: card.pos, sort: card.pos,
title: card.name, title: card.name,
// XXX use the original user? // XXX use the original user?
@ -160,7 +180,7 @@ class TrelloCreator {
// add labels // add labels
if(card.idLabels) { if(card.idLabels) {
cardToCreate.labelIds = card.idLabels.map((trelloId) => { cardToCreate.labelIds = card.idLabels.map((trelloId) => {
return this.labels[trelloId]._id; return this.labels[trelloId];
}); });
} }
// insert card // insert card
@ -205,7 +225,9 @@ class TrelloCreator {
}); });
} }
// XXX add attachments // XXX add attachments
result.push(cardId);
}); });
return result;
} }
getColor(trelloColorCode) { getColor(trelloColorCode) {
@ -225,6 +247,14 @@ class TrelloCreator {
return wekanColor || Boards.simpleSchema()._schema.color.allowedValues[0]; 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) { parseActions(trelloActions) {
trelloActions.forEach((action) => { trelloActions.forEach((action) => {
switch (action.type) { switch (action.type) {
@ -258,19 +288,23 @@ class TrelloCreator {
Meteor.methods({ Meteor.methods({
importTrelloBoard(trelloBoard, data) { importTrelloBoard(trelloBoard, data) {
const trelloCreator = new TrelloCreator(); const trelloCreator = new TrelloCreator();
// 1. check all parameters are ok from a syntax point of view // 1. check all parameters are ok from a syntax point of view
try { try {
// we don't use additional data - this should be an empty object // we don't use additional data - this should be an empty object
check(data, {}); check(data, {});
trelloCreator.checkActions(trelloBoard.actions); trelloCreator.checkActions(trelloBoard.actions);
trelloCreator.checkBoard(trelloBoard); trelloCreator.checkBoard(trelloBoard);
trelloCreator.checkLabels(trelloBoard.labels);
trelloCreator.checkLists(trelloBoard.lists); trelloCreator.checkLists(trelloBoard.lists);
trelloCreator.checkCards(trelloBoard.cards); trelloCreator.checkCards(trelloBoard.cards);
} catch(e) { } catch(e) {
throw new Meteor.Error('error-json-schema'); throw new Meteor.Error('error-json-schema');
} }
// 2. check parameters are ok from a business point of view (exist & authorized) // 2. check parameters are ok from a business point of view (exist & authorized)
// nothing to check, everyone can import boards in their account // nothing to check, everyone can import boards in their account
// 3. create all elements // 3. create all elements
trelloCreator.parseActions(trelloBoard.actions); trelloCreator.parseActions(trelloBoard.actions);
const boardId = trelloCreator.createBoardAndLabels(trelloBoard); const boardId = trelloCreator.createBoardAndLabels(trelloBoard);
@ -279,33 +313,19 @@ Meteor.methods({
// XXX add members // XXX add members
return boardId; return boardId;
}, },
importTrelloCard(trelloCard, data) { importTrelloCard(trelloCard, data) {
const trelloCreator = new TrelloCreator();
// 1. check parameters are ok from a syntax point of view // 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();
});
try { 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(data, { check(data, {
listId: String, listId: String,
sortIndex: Number, sortIndex: Number,
}); });
trelloCreator.checkCards([trelloCard]);
trelloCreator.checkLabels(trelloCard.labels);
trelloCreator.checkActions(trelloCard.actions);
} catch(e) { } catch(e) {
throw new Meteor.Error('error-json-schema'); throw new Meteor.Error('error-json-schema');
} }
@ -321,92 +341,12 @@ Meteor.methods({
} }
} }
// 3. map all fields for the card to create // 3. create all elements
const dateOfImport = new Date(); trelloCreator.lists[trelloCard.idList] = data.listId;
const cardToCreate = { trelloCreator.parseActions(trelloCard.actions);
archived: trelloCard.closed, const board = list.board();
boardId: list.boardId, trelloCreator.createLabels(trelloCard.labels, board);
// this is a default date, we'll fetch the actual one from the actions array const cardIds = trelloCreator.createCardsAndComments([trelloCard], board._id);
createdAt: dateOfImport, return cardIds[0];
dateLastActivity: dateOfImport,
description: trelloCard.desc,
listId: list._id,
sort: data.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 color = currentLabel.color;
const name = currentLabel.name;
const existingLabel = list.board().getLabel(name, color);
let labelId = undefined;
if (existingLabel) {
labelId = existingLabel._id;
} else {
let labelCreated = list.board().addLabel(name, color);
// XXX currently mutations return no value so we have to fetch the label we just created
// waiting on https://github.com/mquandalle/meteor-collection-mutations/issues/1 to remove...
labelCreated = list.board().getLabel(name, color);
labelId = labelCreated._id;
}
if(labelId) {
if (!cardToCreate.labelIds) {
cardToCreate.labelIds = [];
}
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;
}, },
}); });