|
|
|
@ -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]; |
|
|
|
|
}, |
|
|
|
|
}); |
|
|
|
|