ExporterExcel.js 15 KB

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