From 11bf4c7c07865205481c8a4a338736fdc8d29f04 Mon Sep 17 00:00:00 2001 From: Ben0it-T Date: Sun, 3 Oct 2021 09:18:02 +0200 Subject: [PATCH] Fix : export CSV, TSV and XLS translation Feature : add export CSV with semicolon separator --- client/components/sidebar/sidebar.jade | 6 +- client/components/sidebar/sidebar.js | 15 ++++ models/export.js | 37 ++++++--- models/exportExcel.js | 9 ++- models/exporter.js | 103 +++++++------------------ models/server/ExporterExcel.js | 57 ++++++++------ 6 files changed, 115 insertions(+), 112 deletions(-) diff --git a/client/components/sidebar/sidebar.jade b/client/components/sidebar/sidebar.jade index ba62157fb..60825aa1b 100644 --- a/client/components/sidebar/sidebar.jade +++ b/client/components/sidebar/sidebar.jade @@ -400,7 +400,11 @@ template(name="exportBoard") li a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}") i.fa.fa-share-alt - | {{_ 'export-board-csv'}} + | {{_ 'export-board-csv'}} ' , ' + li + a(href="{{exportScsvUrl}}", download="{{exportCsvFilename}}") + i.fa.fa-share-alt + | {{_ 'export-board-csv'}} ' ; ' li a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}") i.fa.fa-share-alt diff --git a/client/components/sidebar/sidebar.js b/client/components/sidebar/sidebar.js index 19b36ccf9..1c3fdfd78 100644 --- a/client/components/sidebar/sidebar.js +++ b/client/components/sidebar/sidebar.js @@ -512,6 +512,21 @@ BlazeComponent.extendComponent({ }; const queryParams = { authToken: Accounts._storedLoginToken(), + delimiter: ',', + }; + return FlowRouter.path( + '/api/boards/:boardId/export/csv', + params, + queryParams, + ); + }, + exportScsvUrl() { + const params = { + boardId: Session.get('currentBoard'), + }; + const queryParams = { + authToken: Accounts._storedLoginToken(), + delimiter: ';', }; return FlowRouter.path( '/api/boards/:boardId/export/csv', diff --git a/models/export.js b/models/export.js index da8e3a491..a28f243ef 100644 --- a/models/export.js +++ b/models/export.js @@ -167,25 +167,38 @@ if (Meteor.isServer) { const exporter = new Exporter(boardId); if (exporter.canExport(user) || impersonateDone) { if (impersonateDone) { - // TODO: Checking for CSV or TSV export type does not work: - // let exportType = 'export' + params.query.delimiter ? 'CSV' : 'TSV'; - // So logging export to CSV: let exportType = 'exportCSV'; + if( params.query.delimiter == "\t" ) { + exportType = 'exportTSV'; + } ImpersonatedUsers.insert({ adminId: adminId, boardId: boardId, reason: exportType, }); } - - body = params.query.delimiter - ? exporter.buildCsv(params.query.delimiter) - : exporter.buildCsv(); - //'Content-Length': body.length, - res.writeHead(200, { - 'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv', - }); - res.write(body); + + let userLanguage = 'en'; + if (user && user.profile) { + userLanguage = user.profile.language + } + + if( params.query.delimiter == "\t" ) { + // TSV file + res.writeHead(200, { + 'Content-Type': 'text/tsv', + }); + } + else { + // CSV file (comma or semicolon) + res.writeHead(200, { + 'Content-Type': 'text/csv; charset=utf-8', + }); + // Adding UTF8 BOM to quick fix MS Excel issue + // use Uint8Array to prevent from converting bytes to string + res.write(new Uint8Array([0xEF, 0xBB, 0xBF])); + } + res.write(exporter.buildCsv(params.query.delimiter, userLanguage)); res.end(); } else { res.writeHead(403); diff --git a/models/exportExcel.js b/models/exportExcel.js index 6ba7effdf..f7f861141 100644 --- a/models/exportExcel.js +++ b/models/exportExcel.js @@ -49,8 +49,13 @@ runOnServer(function() { isAdmin: true, }); } - - const exporterExcel = new ExporterExcel(boardId); + + let userLanguage = 'en'; + if(user && user.profile){ + userLanguage = user.profile.language + } + + const exporterExcel = new ExporterExcel(boardId, userLanguage); if (exporterExcel.canExport(user) || impersonateDone) { if (impersonateDone) { ImpersonatedUsers.insert({ diff --git a/models/exporter.js b/models/exporter.js index 3a671fce4..f05b86881 100644 --- a/models/exporter.js +++ b/models/exporter.js @@ -197,65 +197,43 @@ export class Exporter { return result; } - buildCsv(delimiter = ',') { + buildCsv(userDelimiter = ',', userLanguage='en') { const result = this.build(); const columnHeaders = []; const cardRows = []; const papaconfig = { - delimiter, // get parameter (was: auto-detect) - worker: true, - }; - - /* - newline: "", // auto-detect + quotes: true, quoteChar: '"', escapeChar: '"', + delimiter: userDelimiter, header: true, - transformHeader: undefined, - dynamicTyping: false, - preview: 0, - encoding: "", - comments: false, - step: undefined, - complete: undefined, - error: undefined, - download: false, - downloadRequestHeaders: undefined, - downloadRequestBody: undefined, - skipEmptyLines: false, - chunk: undefined, - chunkSize: undefined, - fastMode: undefined, - beforeFirstChunk: undefined, - withCredentials: undefined, - transform: undefined + newline: "\r\n", + skipEmptyLines: false, + escapeFormulae: true, }; - */ - - //delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP] columnHeaders.push( - 'Title', - 'Description', - 'Status', - 'Swimlane', - 'Owner', - 'Requested by', - 'Assigned by', - 'Members', - 'Assignees', - 'Labels', - 'Start at', - 'Due at', - 'End at', - 'Over time', - 'Spent time (hours)', - 'Created at', - 'Last modified at', - 'Last activity', - 'Vote', - 'Archived', + TAPi18n.__('title','',userLanguage), + TAPi18n.__('description','',userLanguage), + TAPi18n.__('list','',userLanguage), + TAPi18n.__('swimlane','',userLanguage), + TAPi18n.__('owner','',userLanguage), + TAPi18n.__('requested-by','',userLanguage), + TAPi18n.__('assigned-by','',userLanguage), + TAPi18n.__('members','',userLanguage), + TAPi18n.__('assignee','',userLanguage), + TAPi18n.__('labels','',userLanguage), + TAPi18n.__('card-start','',userLanguage), + TAPi18n.__('card-due','',userLanguage), + TAPi18n.__('card-end','',userLanguage), + TAPi18n.__('overtime-hours','',userLanguage), + TAPi18n.__('spent-time-hours','',userLanguage), + TAPi18n.__('createdAt','',userLanguage), + TAPi18n.__('last-modified-at','',userLanguage), + TAPi18n.__('last-activity','',userLanguage), + TAPi18n.__('voting','',userLanguage), + TAPi18n.__('archived','',userLanguage), ); const customFieldMap = {}; let i = 0; @@ -283,30 +261,8 @@ export class Exporter { } i++; }); - cardRows.push([[columnHeaders]]); - /* TODO: Try to get translations working. - These currently only bring English translations. - TAPi18n.__('title'), - TAPi18n.__('description'), - TAPi18n.__('status'), - TAPi18n.__('swimlane'), - TAPi18n.__('owner'), - TAPi18n.__('requested-by'), - TAPi18n.__('assigned-by'), - TAPi18n.__('members'), - TAPi18n.__('assignee'), - TAPi18n.__('labels'), - TAPi18n.__('card-start'), - TAPi18n.__('card-due'), - TAPi18n.__('card-end'), - TAPi18n.__('overtime-hours'), - TAPi18n.__('spent-time-hours'), - TAPi18n.__('createdAt'), - TAPi18n.__('last-modified-at'), - TAPi18n.__('last-activity'), - TAPi18n.__('voting'), - TAPi18n.__('archived'), - */ + //cardRows.push([[columnHeaders]]); + cardRows.push(columnHeaders); result.cards.forEach((card) => { const currentRow = []; @@ -409,7 +365,8 @@ export class Exporter { currentRow.push(customFieldValuesToPush[valueIndex]); } } - cardRows.push([[currentRow]]); + //cardRows.push([[currentRow]]); + cardRows.push(currentRow); }); return Papa.unparse(cardRows, papaconfig); diff --git a/models/server/ExporterExcel.js b/models/server/ExporterExcel.js index e4a4c54c6..0589e4628 100644 --- a/models/server/ExporterExcel.js +++ b/models/server/ExporterExcel.js @@ -3,8 +3,9 @@ import { createWorkbook } from './createWorkbook'; // exporter maybe is broken since Gridfs introduced, add fs and path class ExporterExcel { - constructor(boardId) { + constructor(boardId, userLanguage) { this._boardId = boardId; + this.userLanguage = userLanguage; } build(res) { @@ -157,8 +158,8 @@ class ExporterExcel { //init exceljs workbook const workbook = createWorkbook(); - workbook.creator = TAPi18n.__('export-board'); - workbook.lastModifiedBy = TAPi18n.__('export-board'); + workbook.creator = TAPi18n.__('export-board','',this.userLanguage); + workbook.lastModifiedBy = TAPi18n.__('export-board','',this.userLanguage); workbook.created = new Date(); workbook.modified = new Date(); workbook.lastPrinted = new Date(); @@ -367,11 +368,11 @@ class ExporterExcel { ws.addRow().values = ['', '', '', '', '', '']; //add kanban info ws.addRow().values = [ - TAPi18n.__('createdAt'), + TAPi18n.__('createdAt','',this.userLanguage), addTZhours(result.createdAt), - TAPi18n.__('modifiedAt'), + TAPi18n.__('modifiedAt','',this.userLanguage), addTZhours(result.modifiedAt), - TAPi18n.__('members'), + TAPi18n.__('members','',this.userLanguage), jmem, ]; ws.getRow(3).font = { @@ -388,6 +389,14 @@ class ExporterExcel { }, numFmt: 'yyyy/mm/dd hh:mm:ss', }; + ws.getCell('D3').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 = { @@ -455,24 +464,24 @@ class ExporterExcel { //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'), + TAPi18n.__('number','',this.userLanguage), + TAPi18n.__('title','',this.userLanguage), + TAPi18n.__('description','',this.userLanguage), + TAPi18n.__('parent-card','',this.userLanguage), + TAPi18n.__('owner','',this.userLanguage), + TAPi18n.__('createdAt','',this.userLanguage), + TAPi18n.__('last-modified-at','',this.userLanguage), + TAPi18n.__('card-received','',this.userLanguage), + TAPi18n.__('card-start','',this.userLanguage), + TAPi18n.__('card-due','',this.userLanguage), + TAPi18n.__('card-end','',this.userLanguage), + TAPi18n.__('list','',this.userLanguage), + TAPi18n.__('swimlane','',this.userLanguage), + TAPi18n.__('assignee','',this.userLanguage), + TAPi18n.__('members','',this.userLanguage), + TAPi18n.__('labels','',this.userLanguage), + TAPi18n.__('overtime-hours','',this.userLanguage), + TAPi18n.__('spent-time-hours','',this.userLanguage), ]; ws.getRow(5).height = 20; allBorder('A5');