mirror of
https://github.com/wekan/wekan.git
synced 2025-09-22 01:50:48 +02:00
Fix : export CSV, TSV and XLS translation
Feature : add export CSV with semicolon separator
This commit is contained in:
parent
bc9c7e5aa4
commit
11bf4c7c07
6 changed files with 115 additions and 112 deletions
|
@ -400,7 +400,11 @@ template(name="exportBoard")
|
||||||
li
|
li
|
||||||
a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
|
a(href="{{exportCsvUrl}}", download="{{exportCsvFilename}}")
|
||||||
i.fa.fa-share-alt
|
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
|
li
|
||||||
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
a(href="{{exportTsvUrl}}", download="{{exportTsvFilename}}")
|
||||||
i.fa.fa-share-alt
|
i.fa.fa-share-alt
|
||||||
|
|
|
@ -512,6 +512,21 @@ BlazeComponent.extendComponent({
|
||||||
};
|
};
|
||||||
const queryParams = {
|
const queryParams = {
|
||||||
authToken: Accounts._storedLoginToken(),
|
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(
|
return FlowRouter.path(
|
||||||
'/api/boards/:boardId/export/csv',
|
'/api/boards/:boardId/export/csv',
|
||||||
|
|
|
@ -167,25 +167,38 @@ if (Meteor.isServer) {
|
||||||
const exporter = new Exporter(boardId);
|
const exporter = new Exporter(boardId);
|
||||||
if (exporter.canExport(user) || impersonateDone) {
|
if (exporter.canExport(user) || impersonateDone) {
|
||||||
if (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';
|
let exportType = 'exportCSV';
|
||||||
|
if( params.query.delimiter == "\t" ) {
|
||||||
|
exportType = 'exportTSV';
|
||||||
|
}
|
||||||
ImpersonatedUsers.insert({
|
ImpersonatedUsers.insert({
|
||||||
adminId: adminId,
|
adminId: adminId,
|
||||||
boardId: boardId,
|
boardId: boardId,
|
||||||
reason: exportType,
|
reason: exportType,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
body = params.query.delimiter
|
let userLanguage = 'en';
|
||||||
? exporter.buildCsv(params.query.delimiter)
|
if (user && user.profile) {
|
||||||
: exporter.buildCsv();
|
userLanguage = user.profile.language
|
||||||
//'Content-Length': body.length,
|
}
|
||||||
res.writeHead(200, {
|
|
||||||
'Content-Type': params.query.delimiter ? 'text/csv' : 'text/tsv',
|
if( params.query.delimiter == "\t" ) {
|
||||||
});
|
// TSV file
|
||||||
res.write(body);
|
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();
|
res.end();
|
||||||
} else {
|
} else {
|
||||||
res.writeHead(403);
|
res.writeHead(403);
|
||||||
|
|
|
@ -49,8 +49,13 @@ runOnServer(function() {
|
||||||
isAdmin: true,
|
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 (exporterExcel.canExport(user) || impersonateDone) {
|
||||||
if (impersonateDone) {
|
if (impersonateDone) {
|
||||||
ImpersonatedUsers.insert({
|
ImpersonatedUsers.insert({
|
||||||
|
|
|
@ -197,65 +197,43 @@ export class Exporter {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCsv(delimiter = ',') {
|
buildCsv(userDelimiter = ',', userLanguage='en') {
|
||||||
const result = this.build();
|
const result = this.build();
|
||||||
const columnHeaders = [];
|
const columnHeaders = [];
|
||||||
const cardRows = [];
|
const cardRows = [];
|
||||||
|
|
||||||
const papaconfig = {
|
const papaconfig = {
|
||||||
delimiter, // get parameter (was: auto-detect)
|
quotes: true,
|
||||||
worker: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
newline: "", // auto-detect
|
|
||||||
quoteChar: '"',
|
quoteChar: '"',
|
||||||
escapeChar: '"',
|
escapeChar: '"',
|
||||||
|
delimiter: userDelimiter,
|
||||||
header: true,
|
header: true,
|
||||||
transformHeader: undefined,
|
newline: "\r\n",
|
||||||
dynamicTyping: false,
|
skipEmptyLines: false,
|
||||||
preview: 0,
|
escapeFormulae: true,
|
||||||
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
|
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
//delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
|
|
||||||
|
|
||||||
columnHeaders.push(
|
columnHeaders.push(
|
||||||
'Title',
|
TAPi18n.__('title','',userLanguage),
|
||||||
'Description',
|
TAPi18n.__('description','',userLanguage),
|
||||||
'Status',
|
TAPi18n.__('list','',userLanguage),
|
||||||
'Swimlane',
|
TAPi18n.__('swimlane','',userLanguage),
|
||||||
'Owner',
|
TAPi18n.__('owner','',userLanguage),
|
||||||
'Requested by',
|
TAPi18n.__('requested-by','',userLanguage),
|
||||||
'Assigned by',
|
TAPi18n.__('assigned-by','',userLanguage),
|
||||||
'Members',
|
TAPi18n.__('members','',userLanguage),
|
||||||
'Assignees',
|
TAPi18n.__('assignee','',userLanguage),
|
||||||
'Labels',
|
TAPi18n.__('labels','',userLanguage),
|
||||||
'Start at',
|
TAPi18n.__('card-start','',userLanguage),
|
||||||
'Due at',
|
TAPi18n.__('card-due','',userLanguage),
|
||||||
'End at',
|
TAPi18n.__('card-end','',userLanguage),
|
||||||
'Over time',
|
TAPi18n.__('overtime-hours','',userLanguage),
|
||||||
'Spent time (hours)',
|
TAPi18n.__('spent-time-hours','',userLanguage),
|
||||||
'Created at',
|
TAPi18n.__('createdAt','',userLanguage),
|
||||||
'Last modified at',
|
TAPi18n.__('last-modified-at','',userLanguage),
|
||||||
'Last activity',
|
TAPi18n.__('last-activity','',userLanguage),
|
||||||
'Vote',
|
TAPi18n.__('voting','',userLanguage),
|
||||||
'Archived',
|
TAPi18n.__('archived','',userLanguage),
|
||||||
);
|
);
|
||||||
const customFieldMap = {};
|
const customFieldMap = {};
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
@ -283,30 +261,8 @@ export class Exporter {
|
||||||
}
|
}
|
||||||
i++;
|
i++;
|
||||||
});
|
});
|
||||||
cardRows.push([[columnHeaders]]);
|
//cardRows.push([[columnHeaders]]);
|
||||||
/* TODO: Try to get translations working.
|
cardRows.push(columnHeaders);
|
||||||
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'),
|
|
||||||
*/
|
|
||||||
|
|
||||||
result.cards.forEach((card) => {
|
result.cards.forEach((card) => {
|
||||||
const currentRow = [];
|
const currentRow = [];
|
||||||
|
@ -409,7 +365,8 @@ export class Exporter {
|
||||||
currentRow.push(customFieldValuesToPush[valueIndex]);
|
currentRow.push(customFieldValuesToPush[valueIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cardRows.push([[currentRow]]);
|
//cardRows.push([[currentRow]]);
|
||||||
|
cardRows.push(currentRow);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Papa.unparse(cardRows, papaconfig);
|
return Papa.unparse(cardRows, papaconfig);
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { createWorkbook } from './createWorkbook';
|
||||||
// exporter maybe is broken since Gridfs introduced, add fs and path
|
// exporter maybe is broken since Gridfs introduced, add fs and path
|
||||||
|
|
||||||
class ExporterExcel {
|
class ExporterExcel {
|
||||||
constructor(boardId) {
|
constructor(boardId, userLanguage) {
|
||||||
this._boardId = boardId;
|
this._boardId = boardId;
|
||||||
|
this.userLanguage = userLanguage;
|
||||||
}
|
}
|
||||||
|
|
||||||
build(res) {
|
build(res) {
|
||||||
|
@ -157,8 +158,8 @@ class ExporterExcel {
|
||||||
|
|
||||||
//init exceljs workbook
|
//init exceljs workbook
|
||||||
const workbook = createWorkbook();
|
const workbook = createWorkbook();
|
||||||
workbook.creator = TAPi18n.__('export-board');
|
workbook.creator = TAPi18n.__('export-board','',this.userLanguage);
|
||||||
workbook.lastModifiedBy = TAPi18n.__('export-board');
|
workbook.lastModifiedBy = TAPi18n.__('export-board','',this.userLanguage);
|
||||||
workbook.created = new Date();
|
workbook.created = new Date();
|
||||||
workbook.modified = new Date();
|
workbook.modified = new Date();
|
||||||
workbook.lastPrinted = new Date();
|
workbook.lastPrinted = new Date();
|
||||||
|
@ -367,11 +368,11 @@ class ExporterExcel {
|
||||||
ws.addRow().values = ['', '', '', '', '', ''];
|
ws.addRow().values = ['', '', '', '', '', ''];
|
||||||
//add kanban info
|
//add kanban info
|
||||||
ws.addRow().values = [
|
ws.addRow().values = [
|
||||||
TAPi18n.__('createdAt'),
|
TAPi18n.__('createdAt','',this.userLanguage),
|
||||||
addTZhours(result.createdAt),
|
addTZhours(result.createdAt),
|
||||||
TAPi18n.__('modifiedAt'),
|
TAPi18n.__('modifiedAt','',this.userLanguage),
|
||||||
addTZhours(result.modifiedAt),
|
addTZhours(result.modifiedAt),
|
||||||
TAPi18n.__('members'),
|
TAPi18n.__('members','',this.userLanguage),
|
||||||
jmem,
|
jmem,
|
||||||
];
|
];
|
||||||
ws.getRow(3).font = {
|
ws.getRow(3).font = {
|
||||||
|
@ -388,6 +389,14 @@ class ExporterExcel {
|
||||||
},
|
},
|
||||||
numFmt: 'yyyy/mm/dd hh:mm:ss',
|
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
|
//cell center
|
||||||
function cellCenter(cellno) {
|
function cellCenter(cellno) {
|
||||||
ws.getCell(cellno).alignment = {
|
ws.getCell(cellno).alignment = {
|
||||||
|
@ -455,24 +464,24 @@ class ExporterExcel {
|
||||||
//ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
|
//ws.addRow().values = ['编号', '标题', '创建人', '创建时间', '更新时间', '列表', '成员', '描述', '标签'];
|
||||||
//this is where order in which the excel file generates
|
//this is where order in which the excel file generates
|
||||||
ws.addRow().values = [
|
ws.addRow().values = [
|
||||||
TAPi18n.__('number'),
|
TAPi18n.__('number','',this.userLanguage),
|
||||||
TAPi18n.__('title'),
|
TAPi18n.__('title','',this.userLanguage),
|
||||||
TAPi18n.__('description'),
|
TAPi18n.__('description','',this.userLanguage),
|
||||||
TAPi18n.__('parent-card'),
|
TAPi18n.__('parent-card','',this.userLanguage),
|
||||||
TAPi18n.__('owner'),
|
TAPi18n.__('owner','',this.userLanguage),
|
||||||
TAPi18n.__('createdAt'),
|
TAPi18n.__('createdAt','',this.userLanguage),
|
||||||
TAPi18n.__('last-modified-at'),
|
TAPi18n.__('last-modified-at','',this.userLanguage),
|
||||||
TAPi18n.__('card-received'),
|
TAPi18n.__('card-received','',this.userLanguage),
|
||||||
TAPi18n.__('card-start'),
|
TAPi18n.__('card-start','',this.userLanguage),
|
||||||
TAPi18n.__('card-due'),
|
TAPi18n.__('card-due','',this.userLanguage),
|
||||||
TAPi18n.__('card-end'),
|
TAPi18n.__('card-end','',this.userLanguage),
|
||||||
TAPi18n.__('list'),
|
TAPi18n.__('list','',this.userLanguage),
|
||||||
TAPi18n.__('swimlane'),
|
TAPi18n.__('swimlane','',this.userLanguage),
|
||||||
TAPi18n.__('assignee'),
|
TAPi18n.__('assignee','',this.userLanguage),
|
||||||
TAPi18n.__('members'),
|
TAPi18n.__('members','',this.userLanguage),
|
||||||
TAPi18n.__('labels'),
|
TAPi18n.__('labels','',this.userLanguage),
|
||||||
TAPi18n.__('overtime-hours'),
|
TAPi18n.__('overtime-hours','',this.userLanguage),
|
||||||
TAPi18n.__('spent-time-hours'),
|
TAPi18n.__('spent-time-hours','',this.userLanguage),
|
||||||
];
|
];
|
||||||
ws.getRow(5).height = 20;
|
ws.getRow(5).height = 20;
|
||||||
allBorder('A5');
|
allBorder('A5');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue