Files
wekan/server/methods/csvImportMapping.js
Lauri Ojansivu 86652c8718 Ask for the columns of a CSV or Excel import, apply Owner, and import WeKan's own Excel export.
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 !
2026-10-09 12:52:21 +03:00

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()),
};
},
});