ExporterExcel.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. import { createWorkbook } from './createWorkbook';
  2. // exporter maybe is broken since Gridfs introduced, add fs and path
  3. class ExporterExcel {
  4. constructor(boardId, userLanguage) {
  5. this._boardId = boardId;
  6. this.userLanguage = userLanguage;
  7. }
  8. build(res) {
  9. const fs = Npm.require('fs');
  10. const os = Npm.require('os');
  11. const path = Npm.require('path');
  12. const byBoard = {
  13. boardId: this._boardId,
  14. };
  15. const byBoardNoLinked = {
  16. boardId: this._boardId,
  17. linkedId: {
  18. $in: ['', null],
  19. },
  20. };
  21. // we do not want to retrieve boardId in related elements
  22. const noBoardId = {
  23. fields: {
  24. boardId: 0,
  25. },
  26. };
  27. const result = {
  28. _format: 'wekan-board-1.0.0',
  29. };
  30. _.extend(
  31. result,
  32. Boards.findOne(this._boardId, {
  33. fields: {
  34. stars: 0,
  35. },
  36. }),
  37. );
  38. result.lists = Lists.find(byBoard, noBoardId).fetch();
  39. result.cards = Cards.find(byBoardNoLinked, noBoardId).fetch();
  40. result.swimlanes = Swimlanes.find(byBoard, noBoardId).fetch();
  41. result.customFields = CustomFields.find(
  42. {
  43. boardIds: {
  44. $in: [this.boardId],
  45. },
  46. },
  47. {
  48. fields: {
  49. boardId: 0,
  50. },
  51. },
  52. ).fetch();
  53. result.comments = CardComments.find(byBoard, noBoardId).fetch();
  54. result.activities = Activities.find(byBoard, noBoardId).fetch();
  55. result.rules = Rules.find(byBoard, noBoardId).fetch();
  56. result.checklists = [];
  57. result.checklistItems = [];
  58. result.subtaskItems = [];
  59. result.triggers = [];
  60. result.actions = [];
  61. result.cards.forEach((card) => {
  62. result.checklists.push(
  63. ...Checklists.find({
  64. cardId: card._id,
  65. }).fetch(),
  66. );
  67. result.checklistItems.push(
  68. ...ChecklistItems.find({
  69. cardId: card._id,
  70. }).fetch(),
  71. );
  72. result.subtaskItems.push(
  73. ...Cards.find({
  74. parentId: card._id,
  75. }).fetch(),
  76. );
  77. });
  78. result.rules.forEach((rule) => {
  79. result.triggers.push(
  80. ...Triggers.find(
  81. {
  82. _id: rule.triggerId,
  83. },
  84. noBoardId,
  85. ).fetch(),
  86. );
  87. result.actions.push(
  88. ...Actions.find(
  89. {
  90. _id: rule.actionId,
  91. },
  92. noBoardId,
  93. ).fetch(),
  94. );
  95. });
  96. // we also have to export some user data - as the other elements only
  97. // include id but we have to be careful:
  98. // 1- only exports users that are linked somehow to that board
  99. // 2- do not export any sensitive information
  100. const users = {};
  101. result.members.forEach((member) => {
  102. users[member.userId] = true;
  103. });
  104. result.lists.forEach((list) => {
  105. users[list.userId] = true;
  106. });
  107. result.cards.forEach((card) => {
  108. users[card.userId] = true;
  109. if (card.members) {
  110. card.members.forEach((memberId) => {
  111. users[memberId] = true;
  112. });
  113. }
  114. if (card.assignees) {
  115. card.assignees.forEach((memberId) => {
  116. users[memberId] = true;
  117. });
  118. }
  119. });
  120. result.comments.forEach((comment) => {
  121. users[comment.userId] = true;
  122. });
  123. result.activities.forEach((activity) => {
  124. users[activity.userId] = true;
  125. });
  126. result.checklists.forEach((checklist) => {
  127. users[checklist.userId] = true;
  128. });
  129. const byUserIds = {
  130. _id: {
  131. $in: Object.getOwnPropertyNames(users),
  132. },
  133. };
  134. // we use whitelist to be sure we do not expose inadvertently
  135. // some secret fields that gets added to User later.
  136. const userFields = {
  137. fields: {
  138. _id: 1,
  139. username: 1,
  140. 'profile.initials': 1,
  141. 'profile.avatarUrl': 1,
  142. },
  143. };
  144. result.users = Users.find(byUserIds, userFields)
  145. .fetch()
  146. .map((user) => {
  147. // user avatar is stored as a relative url, we export absolute
  148. if ((user.profile || {}).avatarUrl) {
  149. user.profile.avatarUrl = FlowRouter.url(user.profile.avatarUrl);
  150. }
  151. return user;
  152. });
  153. //init exceljs workbook
  154. const workbook = createWorkbook();
  155. workbook.creator = TAPi18n.__('export-board','',this.userLanguage);
  156. workbook.lastModifiedBy = TAPi18n.__('export-board','',this.userLanguage);
  157. workbook.created = new Date();
  158. workbook.modified = new Date();
  159. workbook.lastPrinted = new Date();
  160. const filename = `${result.title}.xlsx`;
  161. //init worksheet
  162. const worksheet = workbook.addWorksheet(result.title, {
  163. properties: {
  164. tabColor: {
  165. argb: 'FFC0000',
  166. },
  167. },
  168. pageSetup: {
  169. paperSize: 9,
  170. orientation: 'landscape',
  171. },
  172. });
  173. //get worksheet
  174. const ws = workbook.getWorksheet(result.title);
  175. ws.properties.defaultRowHeight = 20;
  176. //init columns
  177. //Excel font. Western: Arial. zh-CN: 宋体
  178. ws.columns = [
  179. {
  180. key: 'a',
  181. width: 14,
  182. },
  183. {
  184. key: 'b',
  185. width: 40,
  186. },
  187. {
  188. key: 'c',
  189. width: 60,
  190. },
  191. {
  192. key: 'd',
  193. width: 40,
  194. },
  195. {
  196. key: 'e',
  197. width: 20,
  198. },
  199. {
  200. key: 'f',
  201. width: 20,
  202. style: {
  203. font: {
  204. name: TAPi18n.__('excel-font'),
  205. size: '10',
  206. },
  207. numFmt: 'yyyy/mm/dd hh:mm:ss',
  208. },
  209. },
  210. {
  211. key: 'g',
  212. width: 20,
  213. style: {
  214. font: {
  215. name: TAPi18n.__('excel-font'),
  216. size: '10',
  217. },
  218. numFmt: 'yyyy/mm/dd hh:mm:ss',
  219. },
  220. },
  221. {
  222. key: 'h',
  223. width: 20,
  224. style: {
  225. font: {
  226. name: TAPi18n.__('excel-font'),
  227. size: '10',
  228. },
  229. numFmt: 'yyyy/mm/dd hh:mm:ss',
  230. },
  231. },
  232. {
  233. key: 'i',
  234. width: 20,
  235. style: {
  236. font: {
  237. name: TAPi18n.__('excel-font'),
  238. size: '10',
  239. },
  240. numFmt: 'yyyy/mm/dd hh:mm:ss',
  241. },
  242. },
  243. {
  244. key: 'j',
  245. width: 20,
  246. style: {
  247. font: {
  248. name: TAPi18n.__('excel-font'),
  249. size: '10',
  250. },
  251. numFmt: 'yyyy/mm/dd hh:mm:ss',
  252. },
  253. },
  254. {
  255. key: 'k',
  256. width: 20,
  257. style: {
  258. font: {
  259. name: TAPi18n.__('excel-font'),
  260. size: '10',
  261. },
  262. numFmt: 'yyyy/mm/dd hh:mm:ss',
  263. },
  264. },
  265. {
  266. key: 'l',
  267. width: 20,
  268. },
  269. {
  270. key: 'm',
  271. width: 20,
  272. },
  273. {
  274. key: 'n',
  275. width: 20,
  276. },
  277. {
  278. key: 'o',
  279. width: 20,
  280. },
  281. {
  282. key: 'p',
  283. width: 20,
  284. },
  285. {
  286. key: 'q',
  287. width: 20,
  288. },
  289. {
  290. key: 'r',
  291. width: 20,
  292. },
  293. ];
  294. //add title line
  295. ws.mergeCells('A1:H1');
  296. ws.getCell('A1').value = result.title;
  297. ws.getCell('A1').style = {
  298. font: {
  299. name: TAPi18n.__('excel-font'),
  300. size: '20',
  301. },
  302. };
  303. ws.getCell('A1').alignment = {
  304. vertical: 'middle',
  305. horizontal: 'center',
  306. };
  307. ws.getRow(1).height = 40;
  308. //get member and assignee info
  309. let jmem = '';
  310. let jassig = '';
  311. const jmeml = {};
  312. const jassigl = {};
  313. for (const i in result.users) {
  314. jmem = `${jmem + result.users[i].username},`;
  315. jmeml[result.users[i]._id] = result.users[i].username;
  316. }
  317. jmem = jmem.substr(0, jmem.length - 1);
  318. for (const ia in result.users) {
  319. jassig = `${jassig + result.users[ia].username},`;
  320. jassigl[result.users[ia]._id] = result.users[ia].username;
  321. }
  322. jassig = jassig.substr(0, jassig.length - 1);
  323. //get kanban list info
  324. const jlist = {};
  325. for (const klist in result.lists) {
  326. jlist[result.lists[klist]._id] = result.lists[klist].title;
  327. }
  328. //get kanban swimlanes info
  329. const jswimlane = {};
  330. for (const kswimlane in result.swimlanes) {
  331. jswimlane[result.swimlanes[kswimlane]._id] =
  332. result.swimlanes[kswimlane].title;
  333. }
  334. //get kanban label info
  335. const jlabel = {};
  336. var isFirst = 1;
  337. for (const klabel in result.labels) {
  338. // console.log(klabel);
  339. if (isFirst == 0) {
  340. jlabel[result.labels[klabel]._id] = `,${result.labels[klabel].name}`;
  341. } else {
  342. isFirst = 0;
  343. jlabel[result.labels[klabel]._id] = result.labels[klabel].name;
  344. }
  345. }
  346. //add data +8 hours
  347. function addTZhours(jdate) {
  348. const curdate = new Date(jdate);
  349. const checkCorrectDate = moment(curdate);
  350. if (checkCorrectDate.isValid()) {
  351. return curdate;
  352. } else {
  353. return ' ';
  354. }
  355. ////Do not add 8 hours to GMT. Use GMT instead.
  356. ////Could not yet figure out how to get localtime.
  357. //return new Date(curdate.setHours(curdate.getHours() + 8));
  358. //return curdate;
  359. }
  360. //add blank row
  361. ws.addRow().values = ['', '', '', '', '', ''];
  362. //add kanban info
  363. ws.addRow().values = [
  364. TAPi18n.__('createdAt','',this.userLanguage),
  365. addTZhours(result.createdAt),
  366. TAPi18n.__('modifiedAt','',this.userLanguage),
  367. addTZhours(result.modifiedAt),
  368. TAPi18n.__('members','',this.userLanguage),
  369. jmem,
  370. ];
  371. ws.getRow(3).font = {
  372. name: TAPi18n.__('excel-font'),
  373. size: 10,
  374. bold: true,
  375. };
  376. ws.mergeCells('F3:R3');
  377. ws.getCell('B3').style = {
  378. font: {
  379. name: TAPi18n.__('excel-font'),
  380. size: '10',
  381. bold: true,
  382. },
  383. numFmt: 'yyyy/mm/dd hh:mm:ss',
  384. };
  385. ws.getCell('D3').style = {
  386. font: {
  387. name: TAPi18n.__('excel-font'),
  388. size: '10',
  389. bold: true,
  390. },
  391. numFmt: 'yyyy/mm/dd hh:mm:ss',
  392. };
  393. //cell center
  394. function cellCenter(cellno) {
  395. ws.getCell(cellno).alignment = {
  396. vertical: 'middle',
  397. horizontal: 'center',
  398. wrapText: true,
  399. };
  400. }
  401. function cellLeft(cellno) {
  402. ws.getCell(cellno).alignment = {
  403. vertical: 'middle',
  404. horizontal: 'left',
  405. wrapText: true,
  406. };
  407. }
  408. cellCenter('A3');
  409. cellCenter('B3');
  410. cellCenter('C3');
  411. cellCenter('D3');
  412. cellCenter('E3');
  413. cellLeft('F3');
  414. ws.getRow(3).height = 20;
  415. //all border
  416. function allBorder(cellno) {
  417. ws.getCell(cellno).border = {
  418. top: {
  419. style: 'thin',
  420. },
  421. left: {
  422. style: 'thin',
  423. },
  424. bottom: {
  425. style: 'thin',
  426. },
  427. right: {
  428. style: 'thin',
  429. },
  430. };
  431. }
  432. allBorder('A3');
  433. allBorder('B3');
  434. allBorder('C3');
  435. allBorder('D3');
  436. allBorder('E3');
  437. allBorder('F3');
  438. //add blank row
  439. ws.addRow().values = [
  440. '',
  441. '',
  442. '',
  443. '',
  444. '',
  445. '',
  446. '',
  447. '',
  448. '',
  449. '',
  450. '',
  451. '',
  452. '',
  453. '',
  454. '',
  455. ];
  456. //add card title
  457. //ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
  458. //this is where order in which the excel file generates
  459. ws.addRow().values = [
  460. TAPi18n.__('number','',this.userLanguage),
  461. TAPi18n.__('title','',this.userLanguage),
  462. TAPi18n.__('description','',this.userLanguage),
  463. TAPi18n.__('parent-card','',this.userLanguage),
  464. TAPi18n.__('owner','',this.userLanguage),
  465. TAPi18n.__('createdAt','',this.userLanguage),
  466. TAPi18n.__('last-modified-at','',this.userLanguage),
  467. TAPi18n.__('card-received','',this.userLanguage),
  468. TAPi18n.__('card-start','',this.userLanguage),
  469. TAPi18n.__('card-due','',this.userLanguage),
  470. TAPi18n.__('card-end','',this.userLanguage),
  471. TAPi18n.__('list','',this.userLanguage),
  472. TAPi18n.__('swimlane','',this.userLanguage),
  473. TAPi18n.__('assignee','',this.userLanguage),
  474. TAPi18n.__('members','',this.userLanguage),
  475. TAPi18n.__('labels','',this.userLanguage),
  476. TAPi18n.__('overtime-hours','',this.userLanguage),
  477. TAPi18n.__('spent-time-hours','',this.userLanguage),
  478. ];
  479. ws.getRow(5).height = 20;
  480. allBorder('A5');
  481. allBorder('B5');
  482. allBorder('C5');
  483. allBorder('D5');
  484. allBorder('E5');
  485. allBorder('F5');
  486. allBorder('G5');
  487. allBorder('H5');
  488. allBorder('I5');
  489. allBorder('J5');
  490. allBorder('K5');
  491. allBorder('L5');
  492. allBorder('M5');
  493. allBorder('N5');
  494. allBorder('O5');
  495. allBorder('P5');
  496. allBorder('Q5');
  497. allBorder('R5');
  498. cellCenter('A5');
  499. cellCenter('B5');
  500. cellCenter('C5');
  501. cellCenter('D5');
  502. cellCenter('E5');
  503. cellCenter('F5');
  504. cellCenter('G5');
  505. cellCenter('H5');
  506. cellCenter('I5');
  507. cellCenter('J5');
  508. cellCenter('K5');
  509. cellCenter('L5');
  510. cellCenter('M5');
  511. cellCenter('N5');
  512. cellCenter('O5');
  513. cellCenter('P5');
  514. cellCenter('Q5');
  515. cellCenter('R5');
  516. ws.getRow(5).font = {
  517. name: TAPi18n.__('excel-font'),
  518. size: 12,
  519. bold: true,
  520. };
  521. //add blank row
  522. //add card info
  523. for (const i in result.cards) {
  524. const jcard = result.cards[i];
  525. //get member info
  526. let jcmem = '';
  527. for (const j in jcard.members) {
  528. jcmem += jmeml[jcard.members[j]];
  529. jcmem += ' ';
  530. }
  531. //get assignee info
  532. let jcassig = '';
  533. for (const ja in jcard.assignees) {
  534. jcassig += jassigl[jcard.assignees[ja]];
  535. jcassig += ' ';
  536. }
  537. //get card label info
  538. let jclabel = '';
  539. for (const jl in jcard.labelIds) {
  540. jclabel += jlabel[jcard.labelIds[jl]];
  541. jclabel += ' ';
  542. }
  543. //get parent name
  544. if (jcard.parentId) {
  545. const parentCard = result.cards.find(
  546. (card) => card._id === jcard.parentId,
  547. );
  548. jcard.parentCardTitle = parentCard ? parentCard.title : '';
  549. }
  550. //add card detail
  551. const t = Number(i) + 1;
  552. ws.addRow().values = [
  553. t.toString(),
  554. jcard.title,
  555. jcard.description,
  556. jcard.parentCardTitle,
  557. jmeml[jcard.userId],
  558. addTZhours(jcard.createdAt),
  559. addTZhours(jcard.dateLastActivity),
  560. addTZhours(jcard.receivedAt),
  561. addTZhours(jcard.startAt),
  562. addTZhours(jcard.dueAt),
  563. addTZhours(jcard.endAt),
  564. jlist[jcard.listId],
  565. jswimlane[jcard.swimlaneId],
  566. jcassig,
  567. jcmem,
  568. jclabel,
  569. jcard.isOvertime ? 'true' : 'false',
  570. jcard.spentTime,
  571. ];
  572. const y = Number(i) + 6;
  573. //ws.getRow(y).height = 25;
  574. allBorder(`A${y}`);
  575. allBorder(`B${y}`);
  576. allBorder(`C${y}`);
  577. allBorder(`D${y}`);
  578. allBorder(`E${y}`);
  579. allBorder(`F${y}`);
  580. allBorder(`G${y}`);
  581. allBorder(`H${y}`);
  582. allBorder(`I${y}`);
  583. allBorder(`J${y}`);
  584. allBorder(`K${y}`);
  585. allBorder(`L${y}`);
  586. allBorder(`M${y}`);
  587. allBorder(`N${y}`);
  588. allBorder(`O${y}`);
  589. allBorder(`P${y}`);
  590. allBorder(`Q${y}`);
  591. allBorder(`R${y}`);
  592. cellCenter(`A${y}`);
  593. ws.getCell(`B${y}`).alignment = {
  594. wrapText: true,
  595. };
  596. ws.getCell(`C${y}`).alignment = {
  597. wrapText: true,
  598. };
  599. ws.getCell(`M${y}`).alignment = {
  600. wrapText: true,
  601. };
  602. ws.getCell(`N${y}`).alignment = {
  603. wrapText: true,
  604. };
  605. ws.getCell(`O${y}`).alignment = {
  606. wrapText: true,
  607. };
  608. }
  609. workbook.xlsx.write(res).then(function () {});
  610. }
  611. canExport(user) {
  612. const board = Boards.findOne(this._boardId);
  613. return board && board.isVisibleBy(user);
  614. }
  615. }
  616. export { ExporterExcel };