import { ReactiveCache } from '/imports/reactiveCache'; import moment from 'moment/min/moment-with-locales'; const Papa = require('papaparse'); import { TAPi18n } from '/imports/i18n'; //const stringify = require('csv-stringify'); //const stringify = require('csv-stringify'); // exporter maybe is broken since Gridfs introduced, add fs and path export class Exporter { constructor(boardId, attachmentId) { this._boardId = boardId; this._attachmentId = attachmentId; } build() { const fs = Npm.require('fs'); const os = Npm.require('os'); const path = Npm.require('path'); const byBoard = { boardId: this._boardId }; const byBoardNoLinked = { boardId: this._boardId, linkedId: { $in: ['', null] }, }; // we do not want to retrieve boardId in related elements const noBoardId = { fields: { boardId: 0, }, }; const result = { _format: 'wekan-board-1.0.0', }; _.extend( result, ReactiveCache.getBoard(this._boardId, { fields: { stars: 0, }, }), ); // [Old] for attachments we only export IDs and absolute url to original doc // [New] Encode attachment to base64 const getBase64Data = function (doc, callback) { let buffer = Buffer.allocUnsafe(0); buffer.fill(0); // callback has the form function (err, res) {} const tmpFile = path.join( os.tmpdir(), `tmpexport${process.pid}${Math.random()}`, ); const tmpWriteable = fs.createWriteStream(tmpFile); const readStream = doc.createReadStream(); readStream.on('data', function (chunk) { buffer = Buffer.concat([buffer, chunk]); }); readStream.on('error', function () { callback(null, null); }); readStream.on('end', function () { // done fs.unlink(tmpFile, () => { //ignored }); callback(null, buffer.toString('base64')); }); readStream.pipe(tmpWriteable); }; const getBase64DataSync = Meteor.wrapAsync(getBase64Data); const byBoardAndAttachment = this._attachmentId ? { boardId: this._boardId, _id: this._attachmentId } : byBoard; result.attachments = ReactiveCache.getAttachments(byBoardAndAttachment) .map((attachment) => { let filebase64 = null; filebase64 = getBase64DataSync(attachment); return { _id: attachment._id, cardId: attachment.meta.cardId, //url: FlowRouter.url(attachment.url()), file: filebase64, name: attachment.name, type: attachment.type, }; }); //When has a especific valid attachment return the single element if (this._attachmentId) { return result.attachments.length > 0 ? result.attachments[0] : {}; } result.lists = ReactiveCache.getLists(byBoard, noBoardId); result.cards = ReactiveCache.getCards(byBoardNoLinked, noBoardId); result.swimlanes = ReactiveCache.getSwimlanes(byBoard, noBoardId); result.customFields = ReactiveCache.getCustomFields( { boardIds: this._boardId }, { fields: { boardIds: 0 } }, ); result.comments = ReactiveCache.getCardComments(byBoard, noBoardId); result.activities = ReactiveCache.getActivities(byBoard, noBoardId); result.rules = ReactiveCache.getRules(byBoard, noBoardId); result.checklists = []; result.checklistItems = []; result.subtaskItems = []; result.triggers = []; result.actions = []; result.cards.forEach((card) => { result.checklists.push( ...ReactiveCache.getChecklists({ cardId: card._id, }), ); result.checklistItems.push( ...ReactiveCache.getChecklistItems({ cardId: card._id, }), ); result.subtaskItems.push( ...ReactiveCache.getCards({ parentId: card._id, }), ); }); result.rules.forEach((rule) => { result.triggers.push( ...ReactiveCache.getTriggers( { _id: rule.triggerId, }, noBoardId, ), ); result.actions.push( ...ReactiveCache.getActions( { _id: rule.actionId, }, noBoardId, ), ); }); // we also have to export some user data - as the other elements only // include id but we have to be careful: // 1- only exports users that are linked somehow to that board // 2- do not export any sensitive information const users = {}; result.members.forEach((member) => { users[member.userId] = true; }); result.lists.forEach((list) => { users[list.userId] = true; }); result.cards.forEach((card) => { users[card.userId] = true; if (card.members) { card.members.forEach((memberId) => { users[memberId] = true; }); } }); result.comments.forEach((comment) => { users[comment.userId] = true; }); result.activities.forEach((activity) => { users[activity.userId] = true; }); result.checklists.forEach((checklist) => { users[checklist.userId] = true; }); const byUserIds = { _id: { $in: Object.getOwnPropertyNames(users), }, }; // we use whitelist to be sure we do not expose inadvertently // some secret fields that gets added to User later. const userFields = { fields: { _id: 1, username: 1, 'profile.fullname': 1, 'profile.initials': 1, 'profile.avatarUrl': 1, }, }; result.users = ReactiveCache.getUsers(byUserIds, userFields) .map((user) => { // user avatar is stored as a relative url, we export absolute if ((user.profile || {}).avatarUrl) { user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl); } return user; }); return result; } buildCsv(userDelimiter = ',', userLanguage='en') { const result = this.build(); const columnHeaders = []; const cardRows = []; const papaconfig = { quotes: true, quoteChar: '"', escapeChar: '"', delimiter: userDelimiter, header: true, newline: "\r\n", skipEmptyLines: false, escapeFormulae: true, }; columnHeaders.push( TAPi18n.__('title','',userLanguage), TAPi18n.__('description','',userLanguage), TAPi18n.__('list','',userLanguage), TAPi18n.__('swimlane','',userLanguage), TAPi18n.__('owner','',userLanguage), TAPi18n.__('requested-by','',userLanguage), TAPi18n.__('assigned-by','',userLanguage), TAPi18n.__('members','',userLanguage), TAPi18n.__('assignee','',userLanguage), TAPi18n.__('labels','',userLanguage), TAPi18n.__('card-start','',userLanguage), TAPi18n.__('card-due','',userLanguage), TAPi18n.__('card-end','',userLanguage), TAPi18n.__('overtime-hours','',userLanguage), TAPi18n.__('spent-time-hours','',userLanguage), TAPi18n.__('createdAt','',userLanguage), TAPi18n.__('last-modified-at','',userLanguage), TAPi18n.__('last-activity','',userLanguage), TAPi18n.__('voting','',userLanguage), TAPi18n.__('archived','',userLanguage), ); const customFieldMap = {}; let i = 0; result.customFields.forEach((customField) => { customFieldMap[customField._id] = { position: i, type: customField.type, }; if (customField.type === 'dropdown') { let options = ''; customField.settings.dropdownItems.forEach((item) => { options = options === '' ? item.name : `${`${options}/${item.name}`}`; }); columnHeaders.push( `CustomField-${customField.name}-${customField.type}-${options}`, ); } else if (customField.type === 'currency') { columnHeaders.push( `CustomField-${customField.name}-${customField.type}-${customField.settings.currencyCode}`, ); } else { columnHeaders.push( `CustomField-${customField.name}-${customField.type}`, ); } i++; }); //cardRows.push([[columnHeaders]]); cardRows.push(columnHeaders); result.cards.forEach((card) => { const currentRow = []; currentRow.push(card.title); currentRow.push(card.description); currentRow.push( result.lists.find(({ _id }) => _id === card.listId).title, ); currentRow.push( result.swimlanes.find(({ _id }) => _id === card.swimlaneId).title, ); currentRow.push( result.users.find(({ _id }) => _id === card.userId).username, ); currentRow.push(card.requestedBy ? card.requestedBy : ' '); currentRow.push(card.assignedBy ? card.assignedBy : ' '); let usernames = ''; card.members.forEach((memberId) => { const user = result.users.find(({ _id }) => _id === memberId); usernames = `${usernames + user.username} `; }); currentRow.push(usernames.trim()); let assignees = ''; card.assignees.forEach((assigneeId) => { const user = result.users.find(({ _id }) => _id === assigneeId); assignees = `${assignees + user.username} `; }); currentRow.push(assignees.trim()); let labels = ''; card.labelIds.forEach((labelId) => { const label = result.labels.find(({ _id }) => _id === labelId); labels = `${labels + label.name}-${label.color} `; }); currentRow.push(labels.trim()); currentRow.push(card.startAt ? moment(card.startAt).format() : ' '); currentRow.push(card.dueAt ? moment(card.dueAt).format() : ' '); currentRow.push(card.endAt ? moment(card.endAt).format() : ' '); currentRow.push(card.isOvertime ? 'true' : 'false'); currentRow.push(card.spentTime); currentRow.push(card.createdAt ? moment(card.createdAt).format() : ' '); currentRow.push(card.modifiedAt ? moment(card.modifiedAt).format() : ' '); currentRow.push( card.dateLastActivity ? moment(card.dateLastActivity).format() : ' ', ); if (card.vote && card.vote.question !== '') { let positiveVoters = ''; let negativeVoters = ''; card.vote.positive.forEach((userId) => { const user = result.users.find(({ _id }) => _id === userId); positiveVoters = `${positiveVoters + user.username} `; }); card.vote.negative.forEach((userId) => { const user = result.users.find(({ _id }) => _id === userId); negativeVoters = `${negativeVoters + user.username} `; }); const votingResult = `${ card.vote.public ? `yes-${ card.vote.positive.length }-${positiveVoters.trimRight()}-no-${ card.vote.negative.length }-${negativeVoters.trimRight()}` : `yes-${card.vote.positive.length}-no-${card.vote.negative.length}` }`; currentRow.push(`${card.vote.question}-${votingResult}`); } else { currentRow.push(' '); } currentRow.push(card.archived ? 'true' : 'false'); //Custom fields const customFieldValuesToPush = new Array(result.customFields.length); card.customFields.forEach((field) => { if (field.value !== null) { if (customFieldMap[field._id].type === 'date') { customFieldValuesToPush[customFieldMap[field._id].position] = moment(field.value).format(); } else if (customFieldMap[field._id].type === 'dropdown') { const dropdownOptions = result.customFields.find( ({ _id }) => _id === field._id, ).settings.dropdownItems; const fieldObj = dropdownOptions.find( ({ _id }) => _id === field.value, ); const fieldValue = (fieldObj && fieldObj.name) || null; customFieldValuesToPush[customFieldMap[field._id].position] = fieldValue; } else { customFieldValuesToPush[customFieldMap[field._id].position] = field.value; } } }); for ( let valueIndex = 0; valueIndex < customFieldValuesToPush.length; valueIndex++ ) { if (!(valueIndex in customFieldValuesToPush)) { currentRow.push(' '); } else { currentRow.push(customFieldValuesToPush[valueIndex]); } } //cardRows.push([[currentRow]]); cardRows.push(currentRow); }); return Papa.unparse(cardRows, papaconfig); } canExport(user) { const board = ReactiveCache.getBoard(this._boardId); return board && board.isVisibleBy(user); } }