Merge pull request #4028 from Ben0it-T/master

Export CSV, TSV and XLS
This commit is contained in:
Lauri Ojansivu 2021-10-03 14:33:28 +03:00 committed by GitHub
commit e6c7463c03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 115 additions and 112 deletions

View file

@ -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

View file

@ -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',

View file

@ -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);

View file

@ -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({

View file

@ -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);

View file

@ -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');