diff --git a/models/exportExcel.js b/models/exportExcel.js index 75d8401cd..6ba7effdf 100644 --- a/models/exportExcel.js +++ b/models/exportExcel.js @@ -1,4 +1,11 @@ -if (Meteor.isServer) { +import { runOnServer } from './runOnServer'; + +runOnServer(function() { + // the ExporterExcel class is only available on server and in order to import + // it here we use runOnServer to have it inside a function instead of an + // if (Meteor.isServer) block + import { ExporterExcel } from './server/ExporterExcel'; + // todo XXX once we have a real API in place, move that route there // todo XXX also share the route definition between the client and the server // so that we could use something like @@ -20,7 +27,6 @@ if (Meteor.isServer) { * @param {string} boardId the ID of the board we are exporting * @param {string} authToken the loginToken */ - const Excel = require('exceljs'); Picker.route('/api/boards/:boardId/exportExcel', function (params, req, res) { const boardId = params.boardId; let user = null; @@ -43,6 +49,7 @@ if (Meteor.isServer) { isAdmin: true, }); } + const exporterExcel = new ExporterExcel(boardId); if (exporterExcel.canExport(user) || impersonateDone) { if (impersonateDone) { @@ -57,619 +64,4 @@ if (Meteor.isServer) { res.end(TAPi18n.__('user-can-not-export-excel')); } }); -} - -// exporter maybe is broken since Gridfs introduced, add fs and path - -export class ExporterExcel { - constructor(boardId) { - this._boardId = boardId; - } - - build(res) { - 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, - Boards.findOne(this._boardId, { - fields: { - stars: 0, - }, - }), - ); - result.lists = Lists.find(byBoard, noBoardId).fetch(); - result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); - result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); - result.customFields = CustomFields.find( - { - boardIds: { - $in: [this.boardId], - }, - }, - { - fields: { - boardId: 0, - }, - }, - ).fetch(); - result.comments = CardComments.find(byBoard, noBoardId).fetch(); - result.activities = Activities.find(byBoard, noBoardId).fetch(); - result.rules = Rules.find(byBoard, noBoardId).fetch(); - result.checklists = []; - result.checklistItems = []; - result.subtaskItems = []; - result.triggers = []; - result.actions = []; - result.cards.forEach((card) => { - result.checklists.push( - ...Checklists.find({ - cardId: card._id, - }).fetch(), - ); - result.checklistItems.push( - ...ChecklistItems.find({ - cardId: card._id, - }).fetch(), - ); - result.subtaskItems.push( - ...Cards.find({ - parentId: card._id, - }).fetch(), - ); - }); - result.rules.forEach((rule) => { - result.triggers.push( - ...Triggers.find( - { - _id: rule.triggerId, - }, - noBoardId, - ).fetch(), - ); - result.actions.push( - ...Actions.find( - { - _id: rule.actionId, - }, - noBoardId, - ).fetch(), - ); - }); - - // 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; - }); - } - if (card.assignees) { - card.assignees.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.initials': 1, - 'profile.avatarUrl': 1, - }, - }; - result.users = Users.find(byUserIds, userFields) - .fetch() - .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; - }); - - //init exceljs workbook - const Excel = require('exceljs'); - const workbook = new Excel.Workbook(); - workbook.creator = TAPi18n.__('export-board'); - workbook.lastModifiedBy = TAPi18n.__('export-board'); - workbook.created = new Date(); - workbook.modified = new Date(); - workbook.lastPrinted = new Date(); - const filename = `${result.title}.xlsx`; - //init worksheet - const worksheet = workbook.addWorksheet(result.title, { - properties: { - tabColor: { - argb: 'FFC0000', - }, - }, - pageSetup: { - paperSize: 9, - orientation: 'landscape', - }, - }); - //get worksheet - const ws = workbook.getWorksheet(result.title); - ws.properties.defaultRowHeight = 20; - //init columns - //Excel font. Western: Arial. zh-CN: 宋体 - ws.columns = [ - { - key: 'a', - width: 14, - }, - { - key: 'b', - width: 40, - }, - { - key: 'c', - width: 60, - }, - { - key: 'd', - width: 40, - }, - { - key: 'e', - width: 20, - }, - { - key: 'f', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'g', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'h', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'i', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'j', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'k', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'l', - width: 20, - }, - { - key: 'm', - width: 20, - }, - { - key: 'n', - width: 20, - }, - { - key: 'o', - width: 20, - }, - { - key: 'p', - width: 20, - }, - { - key: 'q', - width: 20, - }, - { - key: 'r', - width: 20, - }, - ]; - - //add title line - ws.mergeCells('A1:H1'); - ws.getCell('A1').value = result.title; - ws.getCell('A1').style = { - font: { - name: TAPi18n.__('excel-font'), - size: '20', - }, - }; - ws.getCell('A1').alignment = { - vertical: 'middle', - horizontal: 'center', - }; - ws.getRow(1).height = 40; - //get member and assignee info - let jmem = ''; - let jassig = ''; - const jmeml = {}; - const jassigl = {}; - for (const i in result.users) { - jmem = `${jmem + result.users[i].username},`; - jmeml[result.users[i]._id] = result.users[i].username; - } - jmem = jmem.substr(0, jmem.length - 1); - for (const ia in result.users) { - jassig = `${jassig + result.users[ia].username},`; - jassigl[result.users[ia]._id] = result.users[ia].username; - } - jassig = jassig.substr(0, jassig.length - 1); - //get kanban list info - const jlist = {}; - for (const klist in result.lists) { - jlist[result.lists[klist]._id] = result.lists[klist].title; - } - //get kanban swimlanes info - const jswimlane = {}; - for (const kswimlane in result.swimlanes) { - jswimlane[result.swimlanes[kswimlane]._id] = - result.swimlanes[kswimlane].title; - } - //get kanban label info - const jlabel = {}; - var isFirst = 1; - for (const klabel in result.labels) { - // console.log(klabel); - if (isFirst == 0) { - jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`; - } else { - isFirst = 0; - jlabel[result.labels[klabel]._id] = result.labels[klabel].name; - } - } - //add data +8 hours - function addTZhours(jdate) { - const curdate = new Date(jdate); - const checkCorrectDate = moment(curdate); - if (checkCorrectDate.isValid()) { - return curdate; - } else { - return ' '; - } - ////Do not add 8 hours to GMT. Use GMT instead. - ////Could not yet figure out how to get localtime. - //return new Date(curdate.setHours(curdate.getHours() + 8)); - //return curdate; - } - //add blank row - ws.addRow().values = ['', '', '', '', '', '']; - //add kanban info - ws.addRow().values = [ - TAPi18n.__('createdAt'), - addTZhours(result.createdAt), - TAPi18n.__('modifiedAt'), - addTZhours(result.modifiedAt), - TAPi18n.__('members'), - jmem, - ]; - ws.getRow(3).font = { - name: TAPi18n.__('excel-font'), - size: 10, - bold: true, - }; - ws.mergeCells('F3:R3'); - ws.getCell('B3').style = { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - bold: true, - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }; - //cell center - function cellCenter(cellno) { - ws.getCell(cellno).alignment = { - vertical: 'middle', - horizontal: 'center', - wrapText: true, - }; - } - function cellLeft(cellno) { - ws.getCell(cellno).alignment = { - vertical: 'middle', - horizontal: 'left', - wrapText: true, - }; - } - cellCenter('A3'); - cellCenter('B3'); - cellCenter('C3'); - cellCenter('D3'); - cellCenter('E3'); - cellLeft('F3'); - ws.getRow(3).height = 20; - //all border - function allBorder(cellno) { - ws.getCell(cellno).border = { - top: { - style: 'thin', - }, - left: { - style: 'thin', - }, - bottom: { - style: 'thin', - }, - right: { - style: 'thin', - }, - }; - } - allBorder('A3'); - allBorder('B3'); - allBorder('C3'); - allBorder('D3'); - allBorder('E3'); - allBorder('F3'); - //add blank row - ws.addRow().values = [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - ]; - //add card title - //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签']; - //this is where order in which the excel file generates - ws.addRow().values = [ - TAPi18n.__('number'), - TAPi18n.__('title'), - TAPi18n.__('description'), - TAPi18n.__('parent-card'), - TAPi18n.__('owner'), - TAPi18n.__('createdAt'), - TAPi18n.__('last-modified-at'), - TAPi18n.__('card-received'), - TAPi18n.__('card-start'), - TAPi18n.__('card-due'), - TAPi18n.__('card-end'), - TAPi18n.__('list'), - TAPi18n.__('swimlane'), - TAPi18n.__('assignee'), - TAPi18n.__('members'), - TAPi18n.__('labels'), - TAPi18n.__('overtime-hours'), - TAPi18n.__('spent-time-hours'), - ]; - ws.getRow(5).height = 20; - allBorder('A5'); - allBorder('B5'); - allBorder('C5'); - allBorder('D5'); - allBorder('E5'); - allBorder('F5'); - allBorder('G5'); - allBorder('H5'); - allBorder('I5'); - allBorder('J5'); - allBorder('K5'); - allBorder('L5'); - allBorder('M5'); - allBorder('N5'); - allBorder('O5'); - allBorder('P5'); - allBorder('Q5'); - allBorder('R5'); - cellCenter('A5'); - cellCenter('B5'); - cellCenter('C5'); - cellCenter('D5'); - cellCenter('E5'); - cellCenter('F5'); - cellCenter('G5'); - cellCenter('H5'); - cellCenter('I5'); - cellCenter('J5'); - cellCenter('K5'); - cellCenter('L5'); - cellCenter('M5'); - cellCenter('N5'); - cellCenter('O5'); - cellCenter('P5'); - cellCenter('Q5'); - cellCenter('R5'); - ws.getRow(5).font = { - name: TAPi18n.__('excel-font'), - size: 12, - bold: true, - }; - //add blank row - //add card info - for (const i in result.cards) { - const jcard = result.cards[i]; - //get member info - let jcmem = ''; - for (const j in jcard.members) { - jcmem += jmeml[jcard.members[j]]; - jcmem += ' '; - } - //get assignee info - let jcassig = ''; - for (const ja in jcard.assignees) { - jcassig += jassigl[jcard.assignees[ja]]; - jcassig += ' '; - } - //get card label info - let jclabel = ''; - for (const jl in jcard.labelIds) { - jclabel += jlabel[jcard.labelIds[jl]]; - jclabel += ' '; - } - //get parent name - if (jcard.parentId) { - const parentCard = result.cards.find( - (card) => card._id === jcard.parentId, - ); - jcard.parentCardTitle = parentCard ? parentCard.title : ''; - } - - //add card detail - const t = Number(i) + 1; - ws.addRow().values = [ - t.toString(), - jcard.title, - jcard.description, - jcard.parentCardTitle, - jmeml[jcard.userId], - addTZhours(jcard.createdAt), - addTZhours(jcard.dateLastActivity), - addTZhours(jcard.receivedAt), - addTZhours(jcard.startAt), - addTZhours(jcard.dueAt), - addTZhours(jcard.endAt), - jlist[jcard.listId], - jswimlane[jcard.swimlaneId], - jcassig, - jcmem, - jclabel, - jcard.isOvertime ? 'true' : 'false', - jcard.spentTime, - ]; - const y = Number(i) + 6; - //ws.getRow(y).height = 25; - allBorder(`A${y}`); - allBorder(`B${y}`); - allBorder(`C${y}`); - allBorder(`D${y}`); - allBorder(`E${y}`); - allBorder(`F${y}`); - allBorder(`G${y}`); - allBorder(`H${y}`); - allBorder(`I${y}`); - allBorder(`J${y}`); - allBorder(`K${y}`); - allBorder(`L${y}`); - allBorder(`M${y}`); - allBorder(`N${y}`); - allBorder(`O${y}`); - allBorder(`P${y}`); - allBorder(`Q${y}`); - allBorder(`R${y}`); - cellCenter(`A${y}`); - ws.getCell(`B${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`C${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`M${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`N${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`O${y}`).alignment = { - wrapText: true, - }; - } - workbook.xlsx.write(res).then(function () {}); - } - - canExport(user) { - const board = Boards.findOne(this._boardId); - return board && board.isVisibleBy(user); - } -} +}); diff --git a/models/exportPDF.js b/models/exportPDF.js index e9236304d..e698d2f52 100644 --- a/models/exportPDF.js +++ b/models/exportPDF.js @@ -1,4 +1,11 @@ -if (Meteor.isServer) { +import { runOnServer } from './runOnServer'; + +runOnServer(function() { + // the ExporterCardPDF class is only available on server and in order to import + // it here we use runOnServer to have it inside a function instead of an + // if (Meteor.isServer) block + import { ExporterCardPDF } from './server/ExporterCardPDF'; + // todo XXX once we have a real API in place, move that route there // todo XXX also share the route definition between the client and the server // so that we could use something like @@ -20,7 +27,6 @@ if (Meteor.isServer) { * @param {string} boardId the ID of the board we are exporting * @param {string} authToken the loginToken */ - const Excel = require('exceljs'); Picker.route('/api/boards/:boardId/lists/:listId/cards/:cardId/exportPDF', function (params, req, res) { const boardId = params.boardId; const paramListId = req.params.listId; @@ -45,7 +51,8 @@ if (Meteor.isServer) { isAdmin: true, }); } - const exporterExcel = new ExporterCardPDF(boardId); + + const exporterCardPDF = new ExporterCardPDF(boardId); if (exporterCardPDF.canExport(user) || impersonateDone) { if (impersonateDone) { ImpersonatedUsers.insert({ @@ -60,632 +67,4 @@ if (Meteor.isServer) { res.end(TAPi18n.__('user-can-not-export-card-to-pdf')); } }); -} - -// exporter maybe is broken since Gridfs introduced, add fs and path - -export class ExporterCardPDF { - constructor(boardId) { - this._boardId = boardId; - } - - build(res) { - -/* - 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, - Boards.findOne(this._boardId, { - fields: { - stars: 0, - }, - }), - ); - result.lists = Lists.find(byBoard, noBoardId).fetch(); - result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); - result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); - result.customFields = CustomFields.find( - { - boardIds: { - $in: [this.boardId], - }, - }, - { - fields: { - boardId: 0, - }, - }, - ).fetch(); - result.comments = CardComments.find(byBoard, noBoardId).fetch(); - result.activities = Activities.find(byBoard, noBoardId).fetch(); - result.rules = Rules.find(byBoard, noBoardId).fetch(); - result.checklists = []; - result.checklistItems = []; - result.subtaskItems = []; - result.triggers = []; - result.actions = []; - result.cards.forEach((card) => { - result.checklists.push( - ...Checklists.find({ - cardId: card._id, - }).fetch(), - ); - result.checklistItems.push( - ...ChecklistItems.find({ - cardId: card._id, - }).fetch(), - ); - result.subtaskItems.push( - ...Cards.find({ - parentId: card._id, - }).fetch(), - ); - }); - result.rules.forEach((rule) => { - result.triggers.push( - ...Triggers.find( - { - _id: rule.triggerId, - }, - noBoardId, - ).fetch(), - ); - result.actions.push( - ...Actions.find( - { - _id: rule.actionId, - }, - noBoardId, - ).fetch(), - ); - }); - - // 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; - }); - } - if (card.assignees) { - card.assignees.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.initials': 1, - 'profile.avatarUrl': 1, - }, - }; - result.users = Users.find(byUserIds, userFields) - .fetch() - .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; - }); - - //init exceljs workbook - const Excel = require('exceljs'); - const workbook = new Excel.Workbook(); - workbook.creator = TAPi18n.__('export-board'); - workbook.lastModifiedBy = TAPi18n.__('export-board'); - workbook.created = new Date(); - workbook.modified = new Date(); - workbook.lastPrinted = new Date(); - const filename = `${result.title}.xlsx`; - //init worksheet - const worksheet = workbook.addWorksheet(result.title, { - properties: { - tabColor: { - argb: 'FFC0000', - }, - }, - pageSetup: { - paperSize: 9, - orientation: 'landscape', - }, - }); - //get worksheet - const ws = workbook.getWorksheet(result.title); - ws.properties.defaultRowHeight = 20; - //init columns - //Excel font. Western: Arial. zh-CN: 宋体 - ws.columns = [ - { - key: 'a', - width: 14, - }, - { - key: 'b', - width: 40, - }, - { - key: 'c', - width: 60, - }, - { - key: 'd', - width: 40, - }, - { - key: 'e', - width: 20, - }, - { - key: 'f', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'g', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'h', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'i', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'j', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'k', - width: 20, - style: { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }, - }, - { - key: 'l', - width: 20, - }, - { - key: 'm', - width: 20, - }, - { - key: 'n', - width: 20, - }, - { - key: 'o', - width: 20, - }, - { - key: 'p', - width: 20, - }, - { - key: 'q', - width: 20, - }, - { - key: 'r', - width: 20, - }, - ]; - - //add title line - ws.mergeCells('A1:H1'); - ws.getCell('A1').value = result.title; - ws.getCell('A1').style = { - font: { - name: TAPi18n.__('excel-font'), - size: '20', - }, - }; - ws.getCell('A1').alignment = { - vertical: 'middle', - horizontal: 'center', - }; - ws.getRow(1).height = 40; - //get member and assignee info - let jmem = ''; - let jassig = ''; - const jmeml = {}; - const jassigl = {}; - for (const i in result.users) { - jmem = `${jmem + result.users[i].username},`; - jmeml[result.users[i]._id] = result.users[i].username; - } - jmem = jmem.substr(0, jmem.length - 1); - for (const ia in result.users) { - jassig = `${jassig + result.users[ia].username},`; - jassigl[result.users[ia]._id] = result.users[ia].username; - } - jassig = jassig.substr(0, jassig.length - 1); - //get kanban list info - const jlist = {}; - for (const klist in result.lists) { - jlist[result.lists[klist]._id] = result.lists[klist].title; - } - //get kanban swimlanes info - const jswimlane = {}; - for (const kswimlane in result.swimlanes) { - jswimlane[result.swimlanes[kswimlane]._id] = - result.swimlanes[kswimlane].title; - } - //get kanban label info - const jlabel = {}; - var isFirst = 1; - for (const klabel in result.labels) { - // console.log(klabel); - if (isFirst == 0) { - jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`; - } else { - isFirst = 0; - jlabel[result.labels[klabel]._id] = result.labels[klabel].name; - } - } - //add data +8 hours - function addTZhours(jdate) { - const curdate = new Date(jdate); - const checkCorrectDate = moment(curdate); - if (checkCorrectDate.isValid()) { - return curdate; - } else { - return ' '; - } - ////Do not add 8 hours to GMT. Use GMT instead. - ////Could not yet figure out how to get localtime. - //return new Date(curdate.setHours(curdate.getHours() + 8)); - //return curdate; - } - //add blank row - ws.addRow().values = ['', '', '', '', '', '']; - //add kanban info - ws.addRow().values = [ - TAPi18n.__('createdAt'), - addTZhours(result.createdAt), - TAPi18n.__('modifiedAt'), - addTZhours(result.modifiedAt), - TAPi18n.__('members'), - jmem, - ]; - ws.getRow(3).font = { - name: TAPi18n.__('excel-font'), - size: 10, - bold: true, - }; - ws.mergeCells('F3:R3'); - ws.getCell('B3').style = { - font: { - name: TAPi18n.__('excel-font'), - size: '10', - bold: true, - }, - numFmt: 'yyyy/mm/dd hh:mm:ss', - }; - //cell center - function cellCenter(cellno) { - ws.getCell(cellno).alignment = { - vertical: 'middle', - horizontal: 'center', - wrapText: true, - }; - } - function cellLeft(cellno) { - ws.getCell(cellno).alignment = { - vertical: 'middle', - horizontal: 'left', - wrapText: true, - }; - } - cellCenter('A3'); - cellCenter('B3'); - cellCenter('C3'); - cellCenter('D3'); - cellCenter('E3'); - cellLeft('F3'); - ws.getRow(3).height = 20; - //all border - function allBorder(cellno) { - ws.getCell(cellno).border = { - top: { - style: 'thin', - }, - left: { - style: 'thin', - }, - bottom: { - style: 'thin', - }, - right: { - style: 'thin', - }, - }; - } - allBorder('A3'); - allBorder('B3'); - allBorder('C3'); - allBorder('D3'); - allBorder('E3'); - allBorder('F3'); - //add blank row - ws.addRow().values = [ - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - '', - ]; - //add card title - //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签']; - //this is where order in which the excel file generates - ws.addRow().values = [ - TAPi18n.__('number'), - TAPi18n.__('title'), - TAPi18n.__('description'), - TAPi18n.__('parent-card'), - TAPi18n.__('owner'), - TAPi18n.__('createdAt'), - TAPi18n.__('last-modified-at'), - TAPi18n.__('card-received'), - TAPi18n.__('card-start'), - TAPi18n.__('card-due'), - TAPi18n.__('card-end'), - TAPi18n.__('list'), - TAPi18n.__('swimlane'), - TAPi18n.__('assignee'), - TAPi18n.__('members'), - TAPi18n.__('labels'), - TAPi18n.__('overtime-hours'), - TAPi18n.__('spent-time-hours'), - ]; - ws.getRow(5).height = 20; - allBorder('A5'); - allBorder('B5'); - allBorder('C5'); - allBorder('D5'); - allBorder('E5'); - allBorder('F5'); - allBorder('G5'); - allBorder('H5'); - allBorder('I5'); - allBorder('J5'); - allBorder('K5'); - allBorder('L5'); - allBorder('M5'); - allBorder('N5'); - allBorder('O5'); - allBorder('P5'); - allBorder('Q5'); - allBorder('R5'); - cellCenter('A5'); - cellCenter('B5'); - cellCenter('C5'); - cellCenter('D5'); - cellCenter('E5'); - cellCenter('F5'); - cellCenter('G5'); - cellCenter('H5'); - cellCenter('I5'); - cellCenter('J5'); - cellCenter('K5'); - cellCenter('L5'); - cellCenter('M5'); - cellCenter('N5'); - cellCenter('O5'); - cellCenter('P5'); - cellCenter('Q5'); - cellCenter('R5'); - ws.getRow(5).font = { - name: TAPi18n.__('excel-font'), - size: 12, - bold: true, - }; - //add blank row - //add card info - for (const i in result.cards) { - const jcard = result.cards[i]; - //get member info - let jcmem = ''; - for (const j in jcard.members) { - jcmem += jmeml[jcard.members[j]]; - jcmem += ' '; - } - //get assignee info - let jcassig = ''; - for (const ja in jcard.assignees) { - jcassig += jassigl[jcard.assignees[ja]]; - jcassig += ' '; - } - //get card label info - let jclabel = ''; - for (const jl in jcard.labelIds) { - jclabel += jlabel[jcard.labelIds[jl]]; - jclabel += ' '; - } - //get parent name - if (jcard.parentId) { - const parentCard = result.cards.find( - (card) => card._id === jcard.parentId, - ); - jcard.parentCardTitle = parentCard ? parentCard.title : ''; - } - - //add card detail - const t = Number(i) + 1; - ws.addRow().values = [ - t.toString(), - jcard.title, - jcard.description, - jcard.parentCardTitle, - jmeml[jcard.userId], - addTZhours(jcard.createdAt), - addTZhours(jcard.dateLastActivity), - addTZhours(jcard.receivedAt), - addTZhours(jcard.startAt), - addTZhours(jcard.dueAt), - addTZhours(jcard.endAt), - jlist[jcard.listId], - jswimlane[jcard.swimlaneId], - jcassig, - jcmem, - jclabel, - jcard.isOvertime ? 'true' : 'false', - jcard.spentTime, - ]; - const y = Number(i) + 6; - //ws.getRow(y).height = 25; - allBorder(`A${y}`); - allBorder(`B${y}`); - allBorder(`C${y}`); - allBorder(`D${y}`); - allBorder(`E${y}`); - allBorder(`F${y}`); - allBorder(`G${y}`); - allBorder(`H${y}`); - allBorder(`I${y}`); - allBorder(`J${y}`); - allBorder(`K${y}`); - allBorder(`L${y}`); - allBorder(`M${y}`); - allBorder(`N${y}`); - allBorder(`O${y}`); - allBorder(`P${y}`); - allBorder(`Q${y}`); - allBorder(`R${y}`); - cellCenter(`A${y}`); - ws.getCell(`B${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`C${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`M${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`N${y}`).alignment = { - wrapText: true, - }; - ws.getCell(`O${y}`).alignment = { - wrapText: true, - }; - } - workbook.xlsx.write(res).then(function () {}); - */ - - var doc = new PDFDocument({size: 'A4', margin: 50}); - doc.fontSize(12); - doc.text('Some test text', 10, 30, {align: 'center', width: 200}); - this.response.writeHead(200, { - 'Content-type': 'application/pdf', - 'Content-Disposition': "attachment; filename=test.pdf" - }); - this.response.end( doc.outputSync() ); - - } - - canExport(user) { - const board = Boards.findOne(this._boardId); - return board && board.isVisibleBy(user); - } -} +}); diff --git a/models/runOnServer.js b/models/runOnServer.js new file mode 100644 index 000000000..8723b03cd --- /dev/null +++ b/models/runOnServer.js @@ -0,0 +1,8 @@ +/** + * Executes a function only if we are on the server. Use in combination + * with package-sepcific loader functions to create a "nested" import that + * prevents leakage of server-dependencies to the client. + * @param fct {function} the function to be executed on the server + * @return {*} a return value from the function, if there is any + */ +export const runOnServer = fct => Meteor.isServer && fct(); diff --git a/models/server/ExporterCardPDF.js b/models/server/ExporterCardPDF.js new file mode 100644 index 000000000..b226ef805 --- /dev/null +++ b/models/server/ExporterCardPDF.js @@ -0,0 +1,629 @@ +// exporter maybe is broken since Gridfs introduced, add fs and path +import { createWorkbook } from './createWorkbook'; + +class ExporterCardPDF { + constructor(boardId) { + this._boardId = boardId; + } + + build(res) { + + /* + 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, + Boards.findOne(this._boardId, { + fields: { + stars: 0, + }, + }), + ); + result.lists = Lists.find(byBoard, noBoardId).fetch(); + result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); + result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); + result.customFields = CustomFields.find( + { + boardIds: { + $in: [this.boardId], + }, + }, + { + fields: { + boardId: 0, + }, + }, + ).fetch(); + result.comments = CardComments.find(byBoard, noBoardId).fetch(); + result.activities = Activities.find(byBoard, noBoardId).fetch(); + result.rules = Rules.find(byBoard, noBoardId).fetch(); + result.checklists = []; + result.checklistItems = []; + result.subtaskItems = []; + result.triggers = []; + result.actions = []; + result.cards.forEach((card) => { + result.checklists.push( + ...Checklists.find({ + cardId: card._id, + }).fetch(), + ); + result.checklistItems.push( + ...ChecklistItems.find({ + cardId: card._id, + }).fetch(), + ); + result.subtaskItems.push( + ...Cards.find({ + parentId: card._id, + }).fetch(), + ); + }); + result.rules.forEach((rule) => { + result.triggers.push( + ...Triggers.find( + { + _id: rule.triggerId, + }, + noBoardId, + ).fetch(), + ); + result.actions.push( + ...Actions.find( + { + _id: rule.actionId, + }, + noBoardId, + ).fetch(), + ); + }); + + // 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; + }); + } + if (card.assignees) { + card.assignees.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.initials': 1, + 'profile.avatarUrl': 1, + }, + }; + result.users = Users.find(byUserIds, userFields) + .fetch() + .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; + }); + + //init exceljs workbook + const workbook = createWorkbook(); + workbook.creator = TAPi18n.__('export-board'); + workbook.lastModifiedBy = TAPi18n.__('export-board'); + workbook.created = new Date(); + workbook.modified = new Date(); + workbook.lastPrinted = new Date(); + const filename = `${result.title}.xlsx`; + //init worksheet + const worksheet = workbook.addWorksheet(result.title, { + properties: { + tabColor: { + argb: 'FFC0000', + }, + }, + pageSetup: { + paperSize: 9, + orientation: 'landscape', + }, + }); + //get worksheet + const ws = workbook.getWorksheet(result.title); + ws.properties.defaultRowHeight = 20; + //init columns + //Excel font. Western: Arial. zh-CN: 宋体 + ws.columns = [ + { + key: 'a', + width: 14, + }, + { + key: 'b', + width: 40, + }, + { + key: 'c', + width: 60, + }, + { + key: 'd', + width: 40, + }, + { + key: 'e', + width: 20, + }, + { + key: 'f', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'g', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'h', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'i', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'j', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'k', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'l', + width: 20, + }, + { + key: 'm', + width: 20, + }, + { + key: 'n', + width: 20, + }, + { + key: 'o', + width: 20, + }, + { + key: 'p', + width: 20, + }, + { + key: 'q', + width: 20, + }, + { + key: 'r', + width: 20, + }, + ]; + + //add title line + ws.mergeCells('A1:H1'); + ws.getCell('A1').value = result.title; + ws.getCell('A1').style = { + font: { + name: TAPi18n.__('excel-font'), + size: '20', + }, + }; + ws.getCell('A1').alignment = { + vertical: 'middle', + horizontal: 'center', + }; + ws.getRow(1).height = 40; + //get member and assignee info + let jmem = ''; + let jassig = ''; + const jmeml = {}; + const jassigl = {}; + for (const i in result.users) { + jmem = `${jmem + result.users[i].username},`; + jmeml[result.users[i]._id] = result.users[i].username; + } + jmem = jmem.substr(0, jmem.length - 1); + for (const ia in result.users) { + jassig = `${jassig + result.users[ia].username},`; + jassigl[result.users[ia]._id] = result.users[ia].username; + } + jassig = jassig.substr(0, jassig.length - 1); + //get kanban list info + const jlist = {}; + for (const klist in result.lists) { + jlist[result.lists[klist]._id] = result.lists[klist].title; + } + //get kanban swimlanes info + const jswimlane = {}; + for (const kswimlane in result.swimlanes) { + jswimlane[result.swimlanes[kswimlane]._id] = + result.swimlanes[kswimlane].title; + } + //get kanban label info + const jlabel = {}; + var isFirst = 1; + for (const klabel in result.labels) { + // console.log(klabel); + if (isFirst == 0) { + jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`; + } else { + isFirst = 0; + jlabel[result.labels[klabel]._id] = result.labels[klabel].name; + } + } + //add data +8 hours + function addTZhours(jdate) { + const curdate = new Date(jdate); + const checkCorrectDate = moment(curdate); + if (checkCorrectDate.isValid()) { + return curdate; + } else { + return ' '; + } + ////Do not add 8 hours to GMT. Use GMT instead. + ////Could not yet figure out how to get localtime. + //return new Date(curdate.setHours(curdate.getHours() + 8)); + //return curdate; + } + //add blank row + ws.addRow().values = ['', '', '', '', '', '']; + //add kanban info + ws.addRow().values = [ + TAPi18n.__('createdAt'), + addTZhours(result.createdAt), + TAPi18n.__('modifiedAt'), + addTZhours(result.modifiedAt), + TAPi18n.__('members'), + jmem, + ]; + ws.getRow(3).font = { + name: TAPi18n.__('excel-font'), + size: 10, + bold: true, + }; + ws.mergeCells('F3:R3'); + ws.getCell('B3').style = { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + bold: true, + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }; + //cell center + function cellCenter(cellno) { + ws.getCell(cellno).alignment = { + vertical: 'middle', + horizontal: 'center', + wrapText: true, + }; + } + function cellLeft(cellno) { + ws.getCell(cellno).alignment = { + vertical: 'middle', + horizontal: 'left', + wrapText: true, + }; + } + cellCenter('A3'); + cellCenter('B3'); + cellCenter('C3'); + cellCenter('D3'); + cellCenter('E3'); + cellLeft('F3'); + ws.getRow(3).height = 20; + //all border + function allBorder(cellno) { + ws.getCell(cellno).border = { + top: { + style: 'thin', + }, + left: { + style: 'thin', + }, + bottom: { + style: 'thin', + }, + right: { + style: 'thin', + }, + }; + } + allBorder('A3'); + allBorder('B3'); + allBorder('C3'); + allBorder('D3'); + allBorder('E3'); + allBorder('F3'); + //add blank row + ws.addRow().values = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]; + //add card title + //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签']; + //this is where order in which the excel file generates + ws.addRow().values = [ + TAPi18n.__('number'), + TAPi18n.__('title'), + TAPi18n.__('description'), + TAPi18n.__('parent-card'), + TAPi18n.__('owner'), + TAPi18n.__('createdAt'), + TAPi18n.__('last-modified-at'), + TAPi18n.__('card-received'), + TAPi18n.__('card-start'), + TAPi18n.__('card-due'), + TAPi18n.__('card-end'), + TAPi18n.__('list'), + TAPi18n.__('swimlane'), + TAPi18n.__('assignee'), + TAPi18n.__('members'), + TAPi18n.__('labels'), + TAPi18n.__('overtime-hours'), + TAPi18n.__('spent-time-hours'), + ]; + ws.getRow(5).height = 20; + allBorder('A5'); + allBorder('B5'); + allBorder('C5'); + allBorder('D5'); + allBorder('E5'); + allBorder('F5'); + allBorder('G5'); + allBorder('H5'); + allBorder('I5'); + allBorder('J5'); + allBorder('K5'); + allBorder('L5'); + allBorder('M5'); + allBorder('N5'); + allBorder('O5'); + allBorder('P5'); + allBorder('Q5'); + allBorder('R5'); + cellCenter('A5'); + cellCenter('B5'); + cellCenter('C5'); + cellCenter('D5'); + cellCenter('E5'); + cellCenter('F5'); + cellCenter('G5'); + cellCenter('H5'); + cellCenter('I5'); + cellCenter('J5'); + cellCenter('K5'); + cellCenter('L5'); + cellCenter('M5'); + cellCenter('N5'); + cellCenter('O5'); + cellCenter('P5'); + cellCenter('Q5'); + cellCenter('R5'); + ws.getRow(5).font = { + name: TAPi18n.__('excel-font'), + size: 12, + bold: true, + }; + //add blank row + //add card info + for (const i in result.cards) { + const jcard = result.cards[i]; + //get member info + let jcmem = ''; + for (const j in jcard.members) { + jcmem += jmeml[jcard.members[j]]; + jcmem += ' '; + } + //get assignee info + let jcassig = ''; + for (const ja in jcard.assignees) { + jcassig += jassigl[jcard.assignees[ja]]; + jcassig += ' '; + } + //get card label info + let jclabel = ''; + for (const jl in jcard.labelIds) { + jclabel += jlabel[jcard.labelIds[jl]]; + jclabel += ' '; + } + //get parent name + if (jcard.parentId) { + const parentCard = result.cards.find( + (card) => card._id === jcard.parentId, + ); + jcard.parentCardTitle = parentCard ? parentCard.title : ''; + } + + //add card detail + const t = Number(i) + 1; + ws.addRow().values = [ + t.toString(), + jcard.title, + jcard.description, + jcard.parentCardTitle, + jmeml[jcard.userId], + addTZhours(jcard.createdAt), + addTZhours(jcard.dateLastActivity), + addTZhours(jcard.receivedAt), + addTZhours(jcard.startAt), + addTZhours(jcard.dueAt), + addTZhours(jcard.endAt), + jlist[jcard.listId], + jswimlane[jcard.swimlaneId], + jcassig, + jcmem, + jclabel, + jcard.isOvertime ? 'true' : 'false', + jcard.spentTime, + ]; + const y = Number(i) + 6; + //ws.getRow(y).height = 25; + allBorder(`A${y}`); + allBorder(`B${y}`); + allBorder(`C${y}`); + allBorder(`D${y}`); + allBorder(`E${y}`); + allBorder(`F${y}`); + allBorder(`G${y}`); + allBorder(`H${y}`); + allBorder(`I${y}`); + allBorder(`J${y}`); + allBorder(`K${y}`); + allBorder(`L${y}`); + allBorder(`M${y}`); + allBorder(`N${y}`); + allBorder(`O${y}`); + allBorder(`P${y}`); + allBorder(`Q${y}`); + allBorder(`R${y}`); + cellCenter(`A${y}`); + ws.getCell(`B${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`C${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`M${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`N${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`O${y}`).alignment = { + wrapText: true, + }; + } + workbook.xlsx.write(res).then(function () {}); + */ + + var doc = new PDFDocument({size: 'A4', margin: 50}); + doc.fontSize(12); + doc.text('Some test text', 10, 30, {align: 'center', width: 200}); + this.response.writeHead(200, { + 'Content-type': 'application/pdf', + 'Content-Disposition': "attachment; filename=test.pdf" + }); + this.response.end( doc.outputSync() ); + + } + + canExport(user) { + const board = Boards.findOne(this._boardId); + return board && board.isVisibleBy(user); + } +} + +export { ExporterCardPDF }; diff --git a/models/server/ExporterExcel.js b/models/server/ExporterExcel.js new file mode 100644 index 000000000..e4a4c54c6 --- /dev/null +++ b/models/server/ExporterExcel.js @@ -0,0 +1,617 @@ +import { createWorkbook } from './createWorkbook'; + +// exporter maybe is broken since Gridfs introduced, add fs and path + +class ExporterExcel { + constructor(boardId) { + this._boardId = boardId; + } + + build(res) { + 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, + Boards.findOne(this._boardId, { + fields: { + stars: 0, + }, + }), + ); + result.lists = Lists.find(byBoard, noBoardId).fetch(); + result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch(); + result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch(); + result.customFields = CustomFields.find( + { + boardIds: { + $in: [this.boardId], + }, + }, + { + fields: { + boardId: 0, + }, + }, + ).fetch(); + result.comments = CardComments.find(byBoard, noBoardId).fetch(); + result.activities = Activities.find(byBoard, noBoardId).fetch(); + result.rules = Rules.find(byBoard, noBoardId).fetch(); + result.checklists = []; + result.checklistItems = []; + result.subtaskItems = []; + result.triggers = []; + result.actions = []; + result.cards.forEach((card) => { + result.checklists.push( + ...Checklists.find({ + cardId: card._id, + }).fetch(), + ); + result.checklistItems.push( + ...ChecklistItems.find({ + cardId: card._id, + }).fetch(), + ); + result.subtaskItems.push( + ...Cards.find({ + parentId: card._id, + }).fetch(), + ); + }); + result.rules.forEach((rule) => { + result.triggers.push( + ...Triggers.find( + { + _id: rule.triggerId, + }, + noBoardId, + ).fetch(), + ); + result.actions.push( + ...Actions.find( + { + _id: rule.actionId, + }, + noBoardId, + ).fetch(), + ); + }); + + // 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; + }); + } + if (card.assignees) { + card.assignees.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.initials': 1, + 'profile.avatarUrl': 1, + }, + }; + result.users = Users.find(byUserIds, userFields) + .fetch() + .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; + }); + + //init exceljs workbook + const workbook = createWorkbook(); + workbook.creator = TAPi18n.__('export-board'); + workbook.lastModifiedBy = TAPi18n.__('export-board'); + workbook.created = new Date(); + workbook.modified = new Date(); + workbook.lastPrinted = new Date(); + const filename = `${result.title}.xlsx`; + //init worksheet + const worksheet = workbook.addWorksheet(result.title, { + properties: { + tabColor: { + argb: 'FFC0000', + }, + }, + pageSetup: { + paperSize: 9, + orientation: 'landscape', + }, + }); + //get worksheet + const ws = workbook.getWorksheet(result.title); + ws.properties.defaultRowHeight = 20; + //init columns + //Excel font. Western: Arial. zh-CN: 宋体 + ws.columns = [ + { + key: 'a', + width: 14, + }, + { + key: 'b', + width: 40, + }, + { + key: 'c', + width: 60, + }, + { + key: 'd', + width: 40, + }, + { + key: 'e', + width: 20, + }, + { + key: 'f', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'g', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'h', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'i', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'j', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'k', + width: 20, + style: { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }, + }, + { + key: 'l', + width: 20, + }, + { + key: 'm', + width: 20, + }, + { + key: 'n', + width: 20, + }, + { + key: 'o', + width: 20, + }, + { + key: 'p', + width: 20, + }, + { + key: 'q', + width: 20, + }, + { + key: 'r', + width: 20, + }, + ]; + + //add title line + ws.mergeCells('A1:H1'); + ws.getCell('A1').value = result.title; + ws.getCell('A1').style = { + font: { + name: TAPi18n.__('excel-font'), + size: '20', + }, + }; + ws.getCell('A1').alignment = { + vertical: 'middle', + horizontal: 'center', + }; + ws.getRow(1).height = 40; + //get member and assignee info + let jmem = ''; + let jassig = ''; + const jmeml = {}; + const jassigl = {}; + for (const i in result.users) { + jmem = `${jmem + result.users[i].username},`; + jmeml[result.users[i]._id] = result.users[i].username; + } + jmem = jmem.substr(0, jmem.length - 1); + for (const ia in result.users) { + jassig = `${jassig + result.users[ia].username},`; + jassigl[result.users[ia]._id] = result.users[ia].username; + } + jassig = jassig.substr(0, jassig.length - 1); + //get kanban list info + const jlist = {}; + for (const klist in result.lists) { + jlist[result.lists[klist]._id] = result.lists[klist].title; + } + //get kanban swimlanes info + const jswimlane = {}; + for (const kswimlane in result.swimlanes) { + jswimlane[result.swimlanes[kswimlane]._id] = + result.swimlanes[kswimlane].title; + } + //get kanban label info + const jlabel = {}; + var isFirst = 1; + for (const klabel in result.labels) { + // console.log(klabel); + if (isFirst == 0) { + jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`; + } else { + isFirst = 0; + jlabel[result.labels[klabel]._id] = result.labels[klabel].name; + } + } + //add data +8 hours + function addTZhours(jdate) { + const curdate = new Date(jdate); + const checkCorrectDate = moment(curdate); + if (checkCorrectDate.isValid()) { + return curdate; + } else { + return ' '; + } + ////Do not add 8 hours to GMT. Use GMT instead. + ////Could not yet figure out how to get localtime. + //return new Date(curdate.setHours(curdate.getHours() + 8)); + //return curdate; + } + //add blank row + ws.addRow().values = ['', '', '', '', '', '']; + //add kanban info + ws.addRow().values = [ + TAPi18n.__('createdAt'), + addTZhours(result.createdAt), + TAPi18n.__('modifiedAt'), + addTZhours(result.modifiedAt), + TAPi18n.__('members'), + jmem, + ]; + ws.getRow(3).font = { + name: TAPi18n.__('excel-font'), + size: 10, + bold: true, + }; + ws.mergeCells('F3:R3'); + ws.getCell('B3').style = { + font: { + name: TAPi18n.__('excel-font'), + size: '10', + bold: true, + }, + numFmt: 'yyyy/mm/dd hh:mm:ss', + }; + //cell center + function cellCenter(cellno) { + ws.getCell(cellno).alignment = { + vertical: 'middle', + horizontal: 'center', + wrapText: true, + }; + } + function cellLeft(cellno) { + ws.getCell(cellno).alignment = { + vertical: 'middle', + horizontal: 'left', + wrapText: true, + }; + } + cellCenter('A3'); + cellCenter('B3'); + cellCenter('C3'); + cellCenter('D3'); + cellCenter('E3'); + cellLeft('F3'); + ws.getRow(3).height = 20; + //all border + function allBorder(cellno) { + ws.getCell(cellno).border = { + top: { + style: 'thin', + }, + left: { + style: 'thin', + }, + bottom: { + style: 'thin', + }, + right: { + style: 'thin', + }, + }; + } + allBorder('A3'); + allBorder('B3'); + allBorder('C3'); + allBorder('D3'); + allBorder('E3'); + allBorder('F3'); + //add blank row + ws.addRow().values = [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ]; + //add card title + //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签']; + //this is where order in which the excel file generates + ws.addRow().values = [ + TAPi18n.__('number'), + TAPi18n.__('title'), + TAPi18n.__('description'), + TAPi18n.__('parent-card'), + TAPi18n.__('owner'), + TAPi18n.__('createdAt'), + TAPi18n.__('last-modified-at'), + TAPi18n.__('card-received'), + TAPi18n.__('card-start'), + TAPi18n.__('card-due'), + TAPi18n.__('card-end'), + TAPi18n.__('list'), + TAPi18n.__('swimlane'), + TAPi18n.__('assignee'), + TAPi18n.__('members'), + TAPi18n.__('labels'), + TAPi18n.__('overtime-hours'), + TAPi18n.__('spent-time-hours'), + ]; + ws.getRow(5).height = 20; + allBorder('A5'); + allBorder('B5'); + allBorder('C5'); + allBorder('D5'); + allBorder('E5'); + allBorder('F5'); + allBorder('G5'); + allBorder('H5'); + allBorder('I5'); + allBorder('J5'); + allBorder('K5'); + allBorder('L5'); + allBorder('M5'); + allBorder('N5'); + allBorder('O5'); + allBorder('P5'); + allBorder('Q5'); + allBorder('R5'); + cellCenter('A5'); + cellCenter('B5'); + cellCenter('C5'); + cellCenter('D5'); + cellCenter('E5'); + cellCenter('F5'); + cellCenter('G5'); + cellCenter('H5'); + cellCenter('I5'); + cellCenter('J5'); + cellCenter('K5'); + cellCenter('L5'); + cellCenter('M5'); + cellCenter('N5'); + cellCenter('O5'); + cellCenter('P5'); + cellCenter('Q5'); + cellCenter('R5'); + ws.getRow(5).font = { + name: TAPi18n.__('excel-font'), + size: 12, + bold: true, + }; + //add blank row + //add card info + for (const i in result.cards) { + const jcard = result.cards[i]; + //get member info + let jcmem = ''; + for (const j in jcard.members) { + jcmem += jmeml[jcard.members[j]]; + jcmem += ' '; + } + //get assignee info + let jcassig = ''; + for (const ja in jcard.assignees) { + jcassig += jassigl[jcard.assignees[ja]]; + jcassig += ' '; + } + //get card label info + let jclabel = ''; + for (const jl in jcard.labelIds) { + jclabel += jlabel[jcard.labelIds[jl]]; + jclabel += ' '; + } + //get parent name + if (jcard.parentId) { + const parentCard = result.cards.find( + (card) => card._id === jcard.parentId, + ); + jcard.parentCardTitle = parentCard ? parentCard.title : ''; + } + + //add card detail + const t = Number(i) + 1; + ws.addRow().values = [ + t.toString(), + jcard.title, + jcard.description, + jcard.parentCardTitle, + jmeml[jcard.userId], + addTZhours(jcard.createdAt), + addTZhours(jcard.dateLastActivity), + addTZhours(jcard.receivedAt), + addTZhours(jcard.startAt), + addTZhours(jcard.dueAt), + addTZhours(jcard.endAt), + jlist[jcard.listId], + jswimlane[jcard.swimlaneId], + jcassig, + jcmem, + jclabel, + jcard.isOvertime ? 'true' : 'false', + jcard.spentTime, + ]; + const y = Number(i) + 6; + //ws.getRow(y).height = 25; + allBorder(`A${y}`); + allBorder(`B${y}`); + allBorder(`C${y}`); + allBorder(`D${y}`); + allBorder(`E${y}`); + allBorder(`F${y}`); + allBorder(`G${y}`); + allBorder(`H${y}`); + allBorder(`I${y}`); + allBorder(`J${y}`); + allBorder(`K${y}`); + allBorder(`L${y}`); + allBorder(`M${y}`); + allBorder(`N${y}`); + allBorder(`O${y}`); + allBorder(`P${y}`); + allBorder(`Q${y}`); + allBorder(`R${y}`); + cellCenter(`A${y}`); + ws.getCell(`B${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`C${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`M${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`N${y}`).alignment = { + wrapText: true, + }; + ws.getCell(`O${y}`).alignment = { + wrapText: true, + }; + } + workbook.xlsx.write(res).then(function () {}); + } + + canExport(user) { + const board = Boards.findOne(this._boardId); + return board && board.isVisibleBy(user); + } +} + +export { ExporterExcel }; diff --git a/models/server/createWorkbook.js b/models/server/createWorkbook.js new file mode 100644 index 000000000..493f678cd --- /dev/null +++ b/models/server/createWorkbook.js @@ -0,0 +1,5 @@ +import Excel from 'exceljs'; + +export const createWorkbook = function() { + return new Excel.Workbook(); +};