mirror of
https://github.com/wekan/wekan.git
synced 2026-11-07 02:52:33 -05:00
A CSV, TSV or Excel import now shows which column is which card field before importing, pre-filled from the header names - English names and WeKan's own export column names in every language - and asks for what is missing: a file with no list column gets one list, named on the page, instead of one list per row. Unused columns can be kept as text custom fields. The choice goes to the server as csvMapping, checked before anything is written, and the REST import and api.py take one too. The Owner column is applied, as are swimlane, received date, parent card, spent time, overtime and archived. A TSV value containing a comma stays one value. A dropdown custom field value that is not one of its items is skipped instead of failing the import. An Excel workbook is read sheet by sheet, its header found in row 1 or where WeKan's export puts it, in any language; formula cells are read as their results and rich text as text. WeKan's own Excel export imports, and a workbook with many boards - each on its own sheet, as Export all boards writes it - imports as many boards; the Activity sheet is skipped. Thanks to xet7 !
50 lines
2.1 KiB
JavaScript
50 lines
2.1 KiB
JavaScript
// The column mapping step of the CSV/TSV and Excel imports
|
|
// (client/components/import/csvMapping.js) asks the server two things before
|
|
// anything is imported: which field each column of a header is, by every
|
|
// language's names for WeKan's export columns, and - for a workbook, which
|
|
// only the server opens - which sheets are boards and what their header says.
|
|
// Neither writes anything; both answer only a signed-in user, and only while
|
|
// import is enabled, like the import they lead to.
|
|
import { Meteor } from 'meteor/meteor';
|
|
import { check, Match } from 'meteor/check';
|
|
import { assertImportEnabled } from '/models/lib/importExportSecurity';
|
|
import { guessCsvMapping } from '/models/lib/csvImportMapping';
|
|
import { importHeaderNames } from '/server/lib/importHeaderNames';
|
|
import { readExcelBoards } from '/server/lib/excelBoardWorkbook';
|
|
|
|
const MAX_HEADER_CELLS = 10000;
|
|
const SAMPLE_ROWS = 3;
|
|
|
|
const headerCell = Match.Where(value => value == null || typeof value === 'string'
|
|
|| typeof value === 'number' || typeof value === 'boolean');
|
|
|
|
Meteor.methods({
|
|
async csvImportGuessMapping(header) {
|
|
check(header, [headerCell]);
|
|
if (!this.userId) throw new Meteor.Error('error-notAuthorized');
|
|
if (header.length > MAX_HEADER_CELLS) throw new Meteor.Error('invalid-import-format', 'Too many columns');
|
|
await assertImportEnabled();
|
|
return guessCsvMapping(header.map(cell => (cell == null ? '' : String(cell))), await importHeaderNames());
|
|
},
|
|
|
|
async excelImportPreview(excelBase64) {
|
|
check(excelBase64, String);
|
|
if (!this.userId) throw new Meteor.Error('error-notAuthorized');
|
|
await assertImportEnabled();
|
|
let result;
|
|
try {
|
|
result = await readExcelBoards(excelBase64);
|
|
} catch (error) {
|
|
throw new Meteor.Error('invalid-import-format', error.message);
|
|
}
|
|
const first = result.boards[0];
|
|
return {
|
|
header: first.rows[0],
|
|
samples: first.rows.slice(1, 1 + SAMPLE_ROWS),
|
|
boards: result.boards.map(board => ({ title: board.title, sheet: board.sheet, cards: board.rows.length - 1 })),
|
|
skipped: result.skipped,
|
|
mapping: guessCsvMapping(first.rows[0], await importHeaderNames()),
|
|
};
|
|
},
|
|
});
|