const DateString = Match . Where ( function ( dateAsString ) {
check ( dateAsString , String ) ;
return moment ( dateAsString , moment . ISO _8601 ) . isValid ( ) ;
} ) ;
export class TrelloCreator {
constructor ( data ) {
// we log current date, to use the same timestamp for all our actions.
// this helps to retrieve all elements performed by the same import.
this . _nowDate = new Date ( ) ;
// The object creation dates, indexed by Trello id
// (so we only parse actions once!)
this . createdAt = {
board : null ,
cards : { } ,
lists : { } ,
} ;
// The object creator Trello Id, indexed by the object Trello id
// (so we only parse actions once!)
this . createdBy = {
cards : { } , // only cards have a field for that
} ;
// Map of labels Trello ID => Wekan ID
this . labels = { } ;
// Default swimlane
this . swimlane = null ;
// Map of lists Trello ID => Wekan ID
this . lists = { } ;
// Map of cards Trello ID => Wekan ID
this . cards = { } ;
// Map of attachments Wekan ID => Wekan ID
this . attachmentIds = { } ;
// Map of checklists Wekan ID => Wekan ID
this . checklists = { } ;
// The comments, indexed by Trello card id (to map when importing cards)
this . comments = { } ;
// the members, indexed by Trello member id => Wekan user ID
this . members = data . membersMapping ? data . membersMapping : { } ;
// maps a trelloCardId to an array of trelloAttachments
this . attachments = { } ;
}
/ * *
* If dateString is provided ,
* return the Date it represents .
* If not , will return the date when it was first called .
* This is useful for us , as we want all import operations to
* have the exact same date for easier later retrieval .
*
* @ param { String } dateString a properly formatted Date
* /
_now ( dateString ) {
if ( dateString ) {
return new Date ( dateString ) ;
}
if ( ! this . _nowDate ) {
this . _nowDate = new Date ( ) ;
}
return this . _nowDate ;
}
/ * *
* if trelloUserId is provided and we have a mapping ,
* return it .
* Otherwise return current logged user .
* @ param trelloUserId
* @ private
* /
_user ( trelloUserId ) {
if ( trelloUserId && this . members [ trelloUserId ] ) {
return this . members [ trelloUserId ] ;
}
return Meteor . userId ( ) ;
}
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?)
name : String ,
} ) ,
] ) ;
}
checkLists ( trelloLists ) {
check ( trelloLists , [
Match . ObjectIncluding ( {
closed : Boolean ,
name : String ,
} ) ,
] ) ;
}
checkChecklists ( trelloChecklists ) {
check ( trelloChecklists , [
Match . ObjectIncluding ( {
idBoard : String ,
idCard : String ,
name : String ,
checkItems : [
Match . ObjectIncluding ( {
state : String ,
name : String ,
} ) ,
] ,
} ) ,
] ) ;
}
// You must call parseActions before calling this one.
createBoardAndLabels ( trelloBoard ) {
const boardToCreate = {
archived : trelloBoard . closed ,
color : this . getColor ( trelloBoard . prefs . background ) ,
// very old boards won't have a creation activity so no creation date
createdAt : this . _now ( this . createdAt . board ) ,
labels : [ ] ,
members : [
{
userId : Meteor . userId ( ) ,
isAdmin : true ,
isActive : true ,
isNoComments : false ,
isCommentOnly : false ,
swimlaneId : false ,
} ,
] ,
permission : this . getPermission ( trelloBoard . prefs . permissionLevel ) ,
slug : getSlug ( trelloBoard . name ) || 'board' ,
stars : 0 ,
title : trelloBoard . name ,
} ;
// now add other members
if ( trelloBoard . memberships ) {
trelloBoard . memberships . forEach ( trelloMembership => {
const trelloId = trelloMembership . idMember ;
// do we have a mapping?
if ( this . members [ trelloId ] ) {
const wekanId = this . members [ trelloId ] ;
// do we already have it in our list?
const wekanMember = boardToCreate . members . find (
wekanMember => wekanMember . userId === wekanId ,
) ;
if ( wekanMember ) {
// we're already mapped, but maybe with lower rights
if ( ! wekanMember . isAdmin ) {
wekanMember . isAdmin = this . getAdmin ( trelloMembership . memberType ) ;
}
} else {
boardToCreate . members . push ( {
userId : wekanId ,
isAdmin : this . getAdmin ( trelloMembership . memberType ) ,
isActive : true ,
isNoComments : false ,
isCommentOnly : false ,
swimlaneId : false ,
} ) ;
}
}
} ) ;
}
trelloBoard . labels . forEach ( label => {
const labelToCreate = {
_id : Random . id ( 6 ) ,
color : label . color ? label . color : 'black' ,
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 boardId = Boards . direct . insert ( boardToCreate ) ;
Boards . direct . update ( boardId , { $set : { modifiedAt : this . _now ( ) } } ) ;
// log activity
Activities . direct . insert ( {
activityType : 'importBoard' ,
boardId ,
createdAt : this . _now ( ) ,
source : {
id : trelloBoard . id ,
system : 'Trello' ,
url : trelloBoard . url ,
} ,
// We attribute the import to current user,
// not the author from the original object.
userId : this . _user ( ) ,
} ) ;
return boardId ;
}
/ * *
* Create the Wekan cards corresponding to the supplied Trello cards ,
* as well as all linked data : activities , comments , and attachments
* @ param trelloCards
* @ param boardId
* @ returns { Array }
* /
createCards ( trelloCards , boardId ) {
const result = [ ] ;
trelloCards . forEach ( card => {
const cardToCreate = {
archived : card . closed ,
boardId ,
// very old boards won't have a creation activity so no creation date
createdAt : this . _now ( this . createdAt . cards [ card . id ] ) ,
dateLastActivity : this . _now ( ) ,
description : card . desc ,
listId : this . lists [ card . idList ] ,
swimlaneId : this . swimlane ,
sort : card . pos ,
title : card . name ,
// we attribute the card to its creator if available
userId : this . _user ( this . createdBy . cards [ card . id ] ) ,
dueAt : card . due ? this . _now ( card . due ) : null ,
} ;
// add labels
if ( card . idLabels ) {
cardToCreate . labelIds = card . idLabels . map ( trelloId => {
return this . labels [ trelloId ] ;
} ) ;
}
// add members {
if ( card . idMembers ) {
const wekanMembers = [ ] ;
// we can't just map, as some members may not have been mapped
card . idMembers . forEach ( trelloId => {
if ( this . members [ trelloId ] ) {
const wekanId = this . members [ trelloId ] ;
// we may map multiple Trello members to the same wekan user
// in which case we risk adding the same user multiple times
if ( ! wekanMembers . find ( wId => wId === wekanId ) ) {
wekanMembers . push ( wekanId ) ;
}
}
return true ;
} ) ;
if ( wekanMembers . length > 0 ) {
cardToCreate . members = wekanMembers ;
}
}
// add vote
if ( card . idMembersVoted ) {
// Trello only know's positive votes
const positiveVotes = [ ] ;
card . idMembersVoted . forEach ( trelloId => {
if ( this . members [ trelloId ] ) {
const wekanId = this . members [ trelloId ] ;
// we may map multiple Trello members to the same wekan user
// in which case we risk adding the same user multiple times
if ( ! positiveVotes . find ( wId => wId === wekanId ) ) {
positiveVotes . push ( wekanId ) ;
}
}
return true ;
} )
if ( positiveVotes . length > 0 ) {
cardToCreate . vote = {
question : cardToCreate . title ,
public : true ,
positive : positiveVotes ,
}
}
}
// insert card
const cardId = Cards . direct . insert ( cardToCreate ) ;
// keep track of Trello id => Wekan id
this . cards [ card . id ] = cardId ;
// log activity
// Activities.direct.insert({
// activityType: 'importCard',
// boardId,
// cardId,
// createdAt: this._now(),
// listId: cardToCreate.listId,
// source: {
// id: card.id,
// system: 'Trello',
// url: card.url,
// },
// // we attribute the import to current user,
// // not the author of the original card
// userId: this._user(),
// });
// add comments
const comments = this . comments [ card . id ] ;
if ( comments ) {
comments . forEach ( comment => {
const commentToCreate = {
boardId ,
cardId ,
createdAt : this . _now ( comment . date ) ,
text : comment . data . text ,
// we attribute the comment to the original author, default to current user
userId : this . _user ( comment . idMemberCreator ) ,
} ;
// dateLastActivity will be set from activity insert, no need to
// update it ourselves
const commentId = CardComments . direct . insert ( commentToCreate ) ;
// We need to keep adding comment activities this way with Trello
// because it doesn't provide a comment ID
Activities . direct . insert ( {
activityType : 'addComment' ,
boardId : commentToCreate . boardId ,
cardId : commentToCreate . cardId ,
commentId ,
createdAt : this . _now ( comment . date ) ,
// we attribute the addComment (not the import)
// to the original author - it is needed by some UI elements.
userId : commentToCreate . userId ,
} ) ;
} ) ;
}
const attachments = this . attachments [ card . id ] ;
const trelloCoverId = card . idAttachmentCover ;
if ( attachments ) {
attachments . forEach ( att => {
const file = new FS . File ( ) ;
// Simulating file.attachData on the client generates multiple errors
// - HEAD returns null, which causes exception down the line
// - the template then tries to display the url to the attachment which causes other errors
// so we make it server only, and let UI catch up once it is done, forget about latency comp.
const self = this ;
if ( Meteor . isServer ) {
file . attachData ( att . url , function ( error ) {
file . boardId = boardId ;
file . cardId = cardId ;
file . userId = self . _user ( att . idMemberCreator ) ;
// The field source will only be used to prevent adding
// attachments' related activities automatically
file . source = 'import' ;
if ( error ) {
throw error ;
} else {
const wekanAtt = Attachments . insert ( file , ( ) => {
// we do nothing
} ) ;
self . attachmentIds [ att . id ] = wekanAtt . _id ;
//
if ( trelloCoverId === att . id ) {
Cards . direct . update ( cardId , {
$set : { coverId : wekanAtt . _id } ,
} ) ;
}
}
} ) ;
}
// todo XXX set cover - if need be
} ) ;
}
result . push ( cardId ) ;
} ) ;
return result ;
}
// 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 : this . _now ( this . createdAt . lists [ list . id ] ) ,
title : list . name ,
sort : list . pos ,
} ;
const listId = Lists . direct . insert ( listToCreate ) ;
Lists . direct . update ( listId , { $set : { updatedAt : this . _now ( ) } } ) ;
this . lists [ list . id ] = listId ;
// log activity
// Activities.direct.insert({
// activityType: 'importList',
// boardId,
// createdAt: this._now(),
// listId,
// source: {
// id: list.id,
// system: 'Trello',
// },
// // We attribute the import to current user,
// // not the creator of the original object
// userId: this._user(),
// });
} ) ;
}
createSwimlanes ( boardId ) {
const swimlaneToCreate = {
archived : false ,
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
// Wekan boards (eg from 2013) that didn't log the 'createList' action
// we require.
createdAt : this . _now ( ) ,
title : 'Default' ,
sort : 1 ,
} ;
const swimlaneId = Swimlanes . direct . insert ( swimlaneToCreate ) ;
Swimlanes . direct . update ( swimlaneId , { $set : { updatedAt : this . _now ( ) } } ) ;
this . swimlane = swimlaneId ;
}
createChecklists ( trelloChecklists ) {
trelloChecklists . forEach ( checklist => {
if ( this . cards [ checklist . idCard ] ) {
// Create the checklist
const checklistToCreate = {
cardId : this . cards [ checklist . idCard ] ,
title : checklist . name ,
createdAt : this . _now ( ) ,
sort : checklist . pos ,
} ;
const checklistId = Checklists . direct . insert ( checklistToCreate ) ;
// keep track of Trello id => Wekan id
this . checklists [ checklist . id ] = checklistId ;
// Now add the items to the checklistItems
let counter = 0 ;
checklist . checkItems . forEach ( item => {
counter ++ ;
const checklistItemTocreate = {
_id : checklistId + counter ,
title : item . name ,
checklistId : this . checklists [ checklist . id ] ,
cardId : this . cards [ checklist . idCard ] ,
sort : item . pos ,
isFinished : item . state === 'complete' ,
} ;
ChecklistItems . direct . insert ( checklistItemTocreate ) ;
} ) ;
}
} ) ;
}
getAdmin ( trelloMemberType ) {
return trelloMemberType === 'admin' ;
}
getColor ( trelloColorCode ) {
// trello color name => wekan color
const mapColors = {
blue : 'belize' ,
orange : 'pumpkin' ,
green : 'nephritis' ,
red : 'pomegranate' ,
purple : 'wisteria' ,
pink : 'moderatepink' ,
lime : 'limegreen' ,
sky : 'strongcyan' ,
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 => {
if ( action . type === 'addAttachmentToCard' ) {
// We have to be cautious, because the attachment could have been removed later.
// In that case Trello still reports its addition, but removes its 'url' field.
// So we test for that
const trelloAttachment = action . data . attachment ;
// We need the idMemberCreator
trelloAttachment . idMemberCreator = action . idMemberCreator ;
if ( trelloAttachment . url ) {
// we cannot actually create the Wekan attachment, because we don't yet
// have the cards to attach it to, so we store it in the instance variable.
const trelloCardId = action . data . card . id ;
if ( ! this . attachments [ trelloCardId ] ) {
this . attachments [ trelloCardId ] = [ ] ;
}
this . attachments [ trelloCardId ] . push ( trelloAttachment ) ;
}
} else if ( action . type === 'commentCard' ) {
const id = action . data . card . id ;
if ( this . comments [ id ] ) {
this . comments [ id ] . push ( action ) ;
} else {
this . comments [ id ] = [ action ] ;
}
} else if ( action . type === 'createBoard' ) {
this . createdAt . board = action . date ;
} else if ( action . type === 'createCard' ) {
const cardId = action . data . card . id ;
this . createdAt . cards [ cardId ] = action . date ;
this . createdBy . cards [ cardId ] = action . idMemberCreator ;
} else if ( action . type === 'createList' ) {
const listId = action . data . list . id ;
this . createdAt . lists [ listId ] = action . date ;
}
} ) ;
}
importActions ( actions , boardId ) {
actions . forEach ( action => {
switch ( action . type ) {
// Board related actions
// TODO: addBoardMember, removeBoardMember
case 'createBoard' : {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
type : 'board' ,
activityTypeId : boardId ,
activityType : 'createBoard' ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
break ;
}
// List related activities
// TODO: removeList, archivedList
case 'createList' : {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
type : 'list' ,
activityType : 'createList' ,
listId : this . lists [ action . data . list . id ] ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
break ;
}
// Card related activities
// TODO: archivedCard, restoredCard, joinMember, unjoinMember
case 'createCard' : {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
activityType : 'createCard' ,
listId : this . lists [ action . data . list . id ] ,
cardId : this . cards [ action . data . card . id ] ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
break ;
}
case 'updateCard' : {
if ( action . data . old . idList ) {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
oldListId : this . lists [ action . data . old . idList ] ,
activityType : 'moveCard' ,
listId : this . lists [ action . data . listAfter . id ] ,
cardId : this . cards [ action . data . card . id ] ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
}
break ;
}
// Comment related activities
// Trello doesn't export the comment id
// Attachment related activities
case 'addAttachmentToCard' : {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
type : 'card' ,
activityType : 'addAttachment' ,
attachmentId : this . attachmentIds [ action . data . attachment . id ] ,
cardId : this . cards [ action . data . card . id ] ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
break ;
}
// Checklist related activities
case 'addChecklistToCard' : {
Activities . direct . insert ( {
userId : this . _user ( action . idMemberCreator ) ,
activityType : 'addChecklist' ,
cardId : this . cards [ action . data . card . id ] ,
checklistId : this . checklists [ action . data . checklist . id ] ,
boardId ,
createdAt : this . _now ( action . date ) ,
} ) ;
break ;
}
}
// Trello doesn't have an add checklist item action
} ) ;
}
check ( board ) {
try {
// check(data, {
// membersMapping: Match.Optional(Object),
// });
this . checkActions ( board . actions ) ;
this . checkBoard ( board ) ;
this . checkLabels ( board . labels ) ;
this . checkLists ( board . lists ) ;
this . checkCards ( board . cards ) ;
this . checkChecklists ( board . checklists ) ;
} catch ( e ) {
throw new Meteor . Error ( 'error-json-schema' ) ;
}
}
create ( board , currentBoardId ) {
// TODO : Make isSandstorm variable global
const isSandstorm =
Meteor . settings &&
Meteor . settings . public &&
Meteor . settings . public . sandstorm ;
if ( isSandstorm && currentBoardId ) {
const currentBoard = Boards . findOne ( currentBoardId ) ;
currentBoard . archive ( ) ;
}
this . parseActions ( board . actions ) ;
const boardId = this . createBoardAndLabels ( board ) ;
this . createLists ( board . lists , boardId ) ;
this . createSwimlanes ( boardId ) ;
this . createCards ( board . cards , boardId ) ;
this . createChecklists ( board . checklists ) ;
this . importActions ( board . actions , boardId ) ;
// XXX add members
return boardId ;
}
}