From 0f95a513bf8c092d7166a521586333eb2fe8788d Mon Sep 17 00:00:00 2001 From: Lauri Ojansivu Date: Tue, 17 Mar 2020 14:02:43 +0200 Subject: Meteor 1.8 only in use at Sandstorm. --- .sandstorm-meteor-1.8/wekanCreator.js | 853 ++++++++++++++++++++++++++++++++++ 1 file changed, 853 insertions(+) create mode 100644 .sandstorm-meteor-1.8/wekanCreator.js (limited to '.sandstorm-meteor-1.8/wekanCreator.js') diff --git a/.sandstorm-meteor-1.8/wekanCreator.js b/.sandstorm-meteor-1.8/wekanCreator.js new file mode 100644 index 00000000..ec85d93f --- /dev/null +++ b/.sandstorm-meteor-1.8/wekanCreator.js @@ -0,0 +1,853 @@ +const DateString = Match.Where(function(dateAsString) { + check(dateAsString, String); + return moment(dateAsString, moment.ISO_8601).isValid(); +}); + +export class WekanCreator { + 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 Wekan id + // (so we only parse actions once!) + this.createdAt = { + board: null, + cards: {}, + lists: {}, + swimlanes: {}, + }; + // The object creator Wekan Id, indexed by the object Wekan id + // (so we only parse actions once!) + this.createdBy = { + cards: {}, // only cards have a field for that + }; + + // Map of labels Wekan ID => Wekan ID + this.labels = {}; + // Map of swimlanes Wekan ID => Wekan ID + this.swimlanes = {}; + // Map of lists Wekan ID => Wekan ID + this.lists = {}; + // Map of cards Wekan ID => Wekan ID + this.cards = {}; + // Map of comments Wekan ID => Wekan ID + this.commentIds = {}; + // Map of attachments Wekan ID => Wekan ID + this.attachmentIds = {}; + // Map of checklists Wekan ID => Wekan ID + this.checklists = {}; + // Map of checklistItems Wekan ID => Wekan ID + this.checklistItems = {}; + // The comments, indexed by Wekan card id (to map when importing cards) + this.comments = {}; + // Map of rules Wekan ID => Wekan ID + this.rules = {}; + // the members, indexed by Wekan member id => Wekan user ID + this.members = data.membersMapping ? data.membersMapping : {}; + // Map of triggers Wekan ID => Wekan ID + this.triggers = {}; + // Map of actions Wekan ID => Wekan ID + this.actions = {}; + + // maps a wekanCardId to an array of wekanAttachments + 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 wekanUserId is provided and we have a mapping, + * return it. + * Otherwise return current logged user. + * @param wekanUserId + * @private + */ + _user(wekanUserId) { + if (wekanUserId && this.members[wekanUserId]) { + return this.members[wekanUserId]; + } + return Meteor.userId(); + } + + checkActivities(wekanActivities) { + check(wekanActivities, [ + Match.ObjectIncluding({ + activityType: String, + createdAt: DateString, + }), + ]); + // XXX we could perform more thorough checks based on action type + } + + checkBoard(wekanBoard) { + check( + wekanBoard, + Match.ObjectIncluding({ + archived: Boolean, + title: String, + // XXX refine control by validating 'color' against a list of + // allowed values (is it worth the maintenance?) + color: String, + permission: Match.Where(value => { + return ['private', 'public'].indexOf(value) >= 0; + }), + }), + ); + } + + checkCards(wekanCards) { + check(wekanCards, [ + Match.ObjectIncluding({ + archived: Boolean, + dateLastActivity: DateString, + labelIds: [String], + title: String, + sort: Number, + }), + ]); + } + + checkLabels(wekanLabels) { + check(wekanLabels, [ + Match.ObjectIncluding({ + // XXX refine control by validating 'color' against a list of allowed + // values (is it worth the maintenance?) + color: String, + }), + ]); + } + + checkLists(wekanLists) { + check(wekanLists, [ + Match.ObjectIncluding({ + archived: Boolean, + title: String, + }), + ]); + } + + checkSwimlanes(wekanSwimlanes) { + check(wekanSwimlanes, [ + Match.ObjectIncluding({ + archived: Boolean, + title: String, + }), + ]); + } + + checkChecklists(wekanChecklists) { + check(wekanChecklists, [ + Match.ObjectIncluding({ + cardId: String, + title: String, + }), + ]); + } + + checkChecklistItems(wekanChecklistItems) { + check(wekanChecklistItems, [ + Match.ObjectIncluding({ + cardId: String, + title: String, + }), + ]); + } + + checkRules(wekanRules) { + check(wekanRules, [ + Match.ObjectIncluding({ + triggerId: String, + actionId: String, + title: String, + }), + ]); + } + + checkTriggers(wekanTriggers) { + // XXX More check based on trigger type + check(wekanTriggers, [ + Match.ObjectIncluding({ + activityType: String, + desc: String, + }), + ]); + } + + getMembersToMap(data) { + // we will work on the list itself (an ordered array of objects) when a + // mapping is done, we add a 'wekan' field to the object representing the + // imported member + const membersToMap = data.members; + const users = data.users; + // auto-map based on username + membersToMap.forEach(importedMember => { + importedMember.id = importedMember.userId; + delete importedMember.userId; + const user = users.filter(user => { + return user._id === importedMember.id; + })[0]; + if (user.profile && user.profile.fullname) { + importedMember.fullName = user.profile.fullname; + } + importedMember.username = user.username; + const wekanUser = Users.findOne({ username: importedMember.username }); + if (wekanUser) { + importedMember.wekanId = wekanUser._id; + } + }); + return membersToMap; + } + + checkActions(wekanActions) { + // XXX More check based on action type + check(wekanActions, [ + Match.ObjectIncluding({ + actionType: String, + desc: String, + }), + ]); + } + + // You must call parseActions before calling this one. + createBoardAndLabels(boardToImport) { + const boardToCreate = { + archived: boardToImport.archived, + color: boardToImport.color, + // very old boards won't have a creation activity so no creation date + createdAt: this._now(boardToImport.createdAt), + labels: [], + members: [ + { + userId: Meteor.userId(), + wekanId: Meteor.userId(), + isActive: true, + isAdmin: true, + isNoComments: false, + isCommentOnly: false, + swimlaneId: false, + }, + ], + // Standalone Export has modifiedAt missing, adding modifiedAt to fix it + modifiedAt: this._now(boardToImport.modifiedAt), + permission: boardToImport.permission, + slug: getSlug(boardToImport.title) || 'board', + stars: 0, + title: boardToImport.title, + }; + // now add other members + if (boardToImport.members) { + boardToImport.members.forEach(wekanMember => { + // do we already have it in our list? + if ( + !boardToCreate.members.some( + member => member.wekanId === wekanMember.wekanId, + ) + ) + boardToCreate.members.push({ + ...wekanMember, + userId: wekanMember.wekanId, + }); + }); + } + boardToImport.labels.forEach(label => { + const labelToCreate = { + _id: Random.id(6), + color: label.color, + name: label.name, + }; + // We need to remember them by Wekan 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: boardToImport.id, + system: 'Wekan', + }, + // 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 Wekan cards, + * as well as all linked data: activities, comments, and attachments + * @param wekanCards + * @param boardId + * @returns {Array} + */ + createCards(wekanCards, boardId) { + const result = []; + wekanCards.forEach(card => { + const cardToCreate = { + archived: card.archived, + 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.description, + listId: this.lists[card.listId], + swimlaneId: this.swimlanes[card.swimlaneId], + sort: card.sort, + title: card.title, + // we attribute the card to its creator if available + userId: this._user(this.createdBy.cards[card._id]), + isOvertime: card.isOvertime || false, + startAt: card.startAt ? this._now(card.startAt) : null, + dueAt: card.dueAt ? this._now(card.dueAt) : null, + spentTime: card.spentTime || null, + }; + // add labels + if (card.labelIds) { + cardToCreate.labelIds = card.labelIds.map(wekanId => { + return this.labels[wekanId]; + }); + } + // add members { + if (card.members) { + const wekanMembers = []; + // we can't just map, as some members may not have been mapped + card.members.forEach(sourceMemberId => { + if (this.members[sourceMemberId]) { + const wekanId = this.members[sourceMemberId]; + // we may map multiple Wekan 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; + } + } + // set color + if (card.color) { + cardToCreate.color = card.color; + } + // insert card + const cardId = Cards.direct.insert(cardToCreate); + // keep track of Wekan 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: 'Wekan', + // }, + // // 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.createdAt), + text: comment.text, + // we attribute the comment to the original author, default to current user + userId: this._user(comment.userId), + }; + // dateLastActivity will be set from activity insert, no need to + // update it ourselves + const commentId = CardComments.direct.insert(commentToCreate); + this.commentIds[comment._id] = commentId; + // Activities.direct.insert({ + // activityType: 'addComment', + // boardId: commentToCreate.boardId, + // cardId: commentToCreate.cardId, + // commentId, + // createdAt: this._now(commentToCreate.createdAt), + // // 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 wekanCoverId = card.coverId; + 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) { + if (att.url) { + file.attachData(att.url, function(error) { + file.boardId = boardId; + file.cardId = cardId; + file.userId = self._user(att.userId); + // 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 (wekanCoverId === att._id) { + Cards.direct.update(cardId, { + $set: { + coverId: wekanAtt._id, + }, + }); + } + } + }); + } else if (att.file) { + file.attachData( + new Buffer(att.file, 'base64'), + { + type: att.type, + }, + error => { + file.name(att.name); + file.boardId = boardId; + file.cardId = cardId; + file.userId = self._user(att.userId); + // 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 + }); + this.attachmentIds[att._id] = wekanAtt._id; + // + if (wekanCoverId === 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(wekanLabels, board) { + wekanLabels.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(wekanLists, boardId) { + wekanLists.forEach((list, listIndex) => { + const listToCreate = { + archived: list.archived, + 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(this.createdAt.lists[list.id]), + title: list.title, + sort: list.sort ? list.sort : listIndex, + }; + 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: 'Wekan', + // }, + // // We attribute the import to current user, + // // not the creator of the original object + // userId: this._user(), + // }); + }); + } + + createSwimlanes(wekanSwimlanes, boardId) { + wekanSwimlanes.forEach((swimlane, swimlaneIndex) => { + const swimlaneToCreate = { + archived: swimlane.archived, + 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(this.createdAt.swimlanes[swimlane._id]), + title: swimlane.title, + sort: swimlane.sort ? swimlane.sort : swimlaneIndex, + }; + // set color + if (swimlane.color) { + swimlaneToCreate.color = swimlane.color; + } + const swimlaneId = Swimlanes.direct.insert(swimlaneToCreate); + Swimlanes.direct.update(swimlaneId, { + $set: { + updatedAt: this._now(), + }, + }); + this.swimlanes[swimlane._id] = swimlaneId; + }); + } + + createChecklists(wekanChecklists) { + const result = []; + wekanChecklists.forEach((checklist, checklistIndex) => { + // Create the checklist + const checklistToCreate = { + cardId: this.cards[checklist.cardId], + title: checklist.title, + createdAt: checklist.createdAt, + sort: checklist.sort ? checklist.sort : checklistIndex, + }; + const checklistId = Checklists.direct.insert(checklistToCreate); + this.checklists[checklist._id] = checklistId; + result.push(checklistId); + }); + return result; + } + + createTriggers(wekanTriggers, boardId) { + wekanTriggers.forEach(trigger => { + if (trigger.hasOwnProperty('labelId')) { + trigger.labelId = this.labels[trigger.labelId]; + } + if (trigger.hasOwnProperty('memberId')) { + trigger.memberId = this.members[trigger.memberId]; + } + trigger.boardId = boardId; + const oldId = trigger._id; + delete trigger._id; + this.triggers[oldId] = Triggers.direct.insert(trigger); + }); + } + + createActions(wekanActions, boardId) { + wekanActions.forEach(action => { + if (action.hasOwnProperty('labelId')) { + action.labelId = this.labels[action.labelId]; + } + if (action.hasOwnProperty('memberId')) { + action.memberId = this.members[action.memberId]; + } + action.boardId = boardId; + const oldId = action._id; + delete action._id; + this.actions[oldId] = Actions.direct.insert(action); + }); + } + + createRules(wekanRules, boardId) { + wekanRules.forEach(rule => { + // Create the rule + rule.boardId = boardId; + rule.triggerId = this.triggers[rule.triggerId]; + rule.actionId = this.actions[rule.actionId]; + delete rule._id; + Rules.direct.insert(rule); + }); + } + + createChecklistItems(wekanChecklistItems) { + wekanChecklistItems.forEach((checklistitem, checklistitemIndex) => { + // Create the checklistItem + const checklistItemTocreate = { + title: checklistitem.title, + checklistId: this.checklists[checklistitem.checklistId], + cardId: this.cards[checklistitem.cardId], + sort: checklistitem.sort ? checklistitem.sort : checklistitemIndex, + isFinished: checklistitem.isFinished, + }; + const checklistItemId = ChecklistItems.direct.insert( + checklistItemTocreate, + ); + this.checklistItems[checklistitem._id] = checklistItemId; + }); + } + + parseActivities(wekanBoard) { + wekanBoard.activities.forEach(activity => { + switch (activity.activityType) { + case 'addAttachment': { + // We have to be cautious, because the attachment could have been removed later. + // In that case Wekan still reports its addition, but removes its 'url' field. + // So we test for that + const wekanAttachment = wekanBoard.attachments.filter(attachment => { + return attachment._id === activity.attachmentId; + })[0]; + + if (typeof wekanAttachment !== 'undefined' && wekanAttachment) { + if (wekanAttachment.url || wekanAttachment.file) { + // 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 wekanCardId = activity.cardId; + if (!this.attachments[wekanCardId]) { + this.attachments[wekanCardId] = []; + } + this.attachments[wekanCardId].push(wekanAttachment); + } + } + break; + } + case 'addComment': { + const wekanComment = wekanBoard.comments.filter(comment => { + return comment._id === activity.commentId; + })[0]; + const id = activity.cardId; + if (!this.comments[id]) { + this.comments[id] = []; + } + this.comments[id].push(wekanComment); + break; + } + case 'createBoard': { + this.createdAt.board = activity.createdAt; + break; + } + case 'createCard': { + const cardId = activity.cardId; + this.createdAt.cards[cardId] = activity.createdAt; + this.createdBy.cards[cardId] = activity.userId; + break; + } + case 'createList': { + const listId = activity.listId; + this.createdAt.lists[listId] = activity.createdAt; + break; + } + case 'createSwimlane': { + const swimlaneId = activity.swimlaneId; + this.createdAt.swimlanes[swimlaneId] = activity.createdAt; + break; + } + } + }); + } + + importActivities(activities, boardId) { + activities.forEach(activity => { + switch (activity.activityType) { + // Board related activities + // TODO: addBoardMember, removeBoardMember + case 'createBoard': { + Activities.direct.insert({ + userId: this._user(activity.userId), + type: 'board', + activityTypeId: boardId, + activityType: activity.activityType, + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + // List related activities + // TODO: removeList, archivedList + case 'createList': { + Activities.direct.insert({ + userId: this._user(activity.userId), + type: 'list', + activityType: activity.activityType, + listId: this.lists[activity.listId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + // Card related activities + // TODO: archivedCard, restoredCard, joinMember, unjoinMember + case 'createCard': { + Activities.direct.insert({ + userId: this._user(activity.userId), + activityType: activity.activityType, + listId: this.lists[activity.listId], + cardId: this.cards[activity.cardId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + case 'moveCard': { + Activities.direct.insert({ + userId: this._user(activity.userId), + oldListId: this.lists[activity.oldListId], + activityType: activity.activityType, + listId: this.lists[activity.listId], + cardId: this.cards[activity.cardId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + // Comment related activities + case 'addComment': { + Activities.direct.insert({ + userId: this._user(activity.userId), + activityType: activity.activityType, + cardId: this.cards[activity.cardId], + commentId: this.commentIds[activity.commentId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + // Attachment related activities + case 'addAttachment': { + Activities.direct.insert({ + userId: this._user(activity.userId), + type: 'card', + activityType: activity.activityType, + attachmentId: this.attachmentIds[activity.attachmentId], + cardId: this.cards[activity.cardId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + // Checklist related activities + case 'addChecklist': { + Activities.direct.insert({ + userId: this._user(activity.userId), + activityType: activity.activityType, + cardId: this.cards[activity.cardId], + checklistId: this.checklists[activity.checklistId], + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + case 'addChecklistItem': { + Activities.direct.insert({ + userId: this._user(activity.userId), + activityType: activity.activityType, + cardId: this.cards[activity.cardId], + checklistId: this.checklists[activity.checklistId], + checklistItemId: activity.checklistItemId.replace( + activity.checklistId, + this.checklists[activity.checklistId], + ), + boardId, + createdAt: this._now(activity.createdAt), + }); + break; + } + } + }); + } + + //check(board) { + check() { + //try { + // check(data, { + // membersMapping: Match.Optional(Object), + // }); + // this.checkActivities(board.activities); + // this.checkBoard(board); + // this.checkLabels(board.labels); + // this.checkLists(board.lists); + // this.checkSwimlanes(board.swimlanes); + // this.checkCards(board.cards); + //this.checkChecklists(board.checklists); + // this.checkRules(board.rules); + // this.checkActions(board.actions); + //this.checkTriggers(board.triggers); + //this.checkChecklistItems(board.checklistItems); + //} 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.parseActivities(board); + const boardId = this.createBoardAndLabels(board); + this.createLists(board.lists, boardId); + this.createSwimlanes(board.swimlanes, boardId); + this.createCards(board.cards, boardId); + this.createChecklists(board.checklists); + this.createChecklistItems(board.checklistItems); + this.importActivities(board.activities, boardId); + this.createTriggers(board.triggers, boardId); + this.createActions(board.actions, boardId); + this.createRules(board.rules, boardId); + // XXX add members + return boardId; + } +} -- cgit v1.2.3-1-g7c22