ソースを参照

remove unused exceljs from client bundle

Jan Küster 4 年 前
コミット
3aa7a6e244

+ 10 - 618
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);
-  }
-}
+});

+ 11 - 632
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);
-  }
-}
+});

+ 8 - 0
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();

+ 629 - 0
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 };

+ 617 - 0
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 };

+ 5 - 0
models/server/createWorkbook.js

@@ -0,0 +1,5 @@
+import Excel from 'exceljs';
+
+export const createWorkbook = function() {
+  return new Excel.Workbook();
+};