mirror of https://github.com/nocodb/nocodb
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
7.5 KiB
201 lines
7.5 KiB
import XLSX from 'xlsx' |
|
import { UITypes } from '~/components/project/spreadsheet/helpers/uiTypes' |
|
import TemplateGenerator from '~/components/import/templateParsers/TemplateGenerator' |
|
import { getCheckboxValue, isCheckboxType } from '~/components/import/templateParsers/parserHelpers' |
|
|
|
const excelTypeToUidt = { |
|
d: UITypes.DateTime, |
|
b: UITypes.Checkbox, |
|
n: UITypes.Number, |
|
s: UITypes.SingleLineText |
|
} |
|
|
|
export default class ExcelTemplateAdapter extends TemplateGenerator { |
|
constructor(name, ab, parserConfig = {}) { |
|
super() |
|
this.config = { |
|
maxRowsToParse: 500, |
|
...parserConfig |
|
} |
|
this.name = name |
|
this.excelData = ab |
|
this.project = { |
|
title: this.name, |
|
tables: [] |
|
} |
|
this.data = {} |
|
} |
|
|
|
async init() { |
|
this.wb = XLSX.read(new Uint8Array(this.excelData), { type: 'array', cellText: true, cellDates: true }) |
|
} |
|
|
|
parse() { |
|
const tableNamePrefixRef = {} |
|
for (let i = 0; i < this.wb.SheetNames.length; i++) { |
|
const columnNamePrefixRef = { id: 0 } |
|
const sheet = this.wb.SheetNames[i] |
|
let tn = (sheet || 'table').replace(/\W/g, '_').trim() |
|
|
|
while (tn in tableNamePrefixRef) { |
|
tn = `${tn}${++tableNamePrefixRef[tn]}` |
|
} |
|
tableNamePrefixRef[tn] = 0 |
|
|
|
const table = { table_name: tn, ref_table_name: tn, columns: [] } |
|
this.data[tn] = [] |
|
const ws = this.wb.Sheets[sheet] |
|
const range = XLSX.utils.decode_range(ws['!ref']) |
|
const rows = XLSX.utils.sheet_to_json(ws, { header: 1, blankrows: false, cellDates: true, defval: null }) |
|
|
|
if (this.name.slice(-3) !== 'csv') { |
|
// fix precision bug & timezone offset issues introduced by xlsx |
|
const basedate = new Date(1899, 11, 30, 0, 0, 0) |
|
// number of milliseconds since base date |
|
const dnthresh = basedate.getTime() + (new Date().getTimezoneOffset() - basedate.getTimezoneOffset()) * 60000 |
|
// number of milliseconds in a day |
|
const day_ms = 24 * 60 * 60 * 1000 |
|
// handle date1904 property |
|
const fixImportedDate = (date) => { |
|
const parsed = XLSX.SSF.parse_date_code((date.getTime() - dnthresh) / day_ms, { |
|
date1904: this.wb.Workbook.WBProps.date1904 |
|
}) |
|
return new Date(parsed.y, parsed.m, parsed.d, parsed.H, parsed.M, parsed.S) |
|
} |
|
// fix imported date |
|
rows = rows.map(r => r.map((v) => { |
|
return v instanceof Date ? fixImportedDate(v) : v |
|
})) |
|
} |
|
|
|
const columnNameRowExist = +rows[0].every(v => v === null || typeof v === 'string') |
|
|
|
// const colLen = Math.max() |
|
for (let col = 0; col < rows[0].length; col++) { |
|
let cn = ((columnNameRowExist && rows[0] && rows[0][col] && rows[0][col].toString().trim()) || |
|
`field_${col + 1}`).replace(/\W/g, '_').trim() |
|
|
|
while (cn in columnNamePrefixRef) { |
|
cn = `${cn}${++columnNamePrefixRef[cn]}` |
|
} |
|
columnNamePrefixRef[cn] = 0 |
|
|
|
const column = { |
|
column_name: cn, |
|
ref_column_name: cn |
|
} |
|
|
|
table.columns.push(column) |
|
|
|
// const cellId = `${col.toString(26).split('').map(s => (parseInt(s, 26) + 10).toString(36).toUpperCase())}2`; |
|
const cellId = XLSX.utils.encode_cell({ |
|
c: range.s.c + col, |
|
r: columnNameRowExist |
|
}) |
|
const cellProps = ws[cellId] || {} |
|
column.uidt = excelTypeToUidt[cellProps.t] || UITypes.SingleLineText |
|
|
|
// todo: optimize |
|
if (column.uidt === UITypes.SingleLineText) { |
|
// check for long text |
|
if (rows.some(r => |
|
(r[col] || '').toString().match(/[\r\n]/) || |
|
(r[col] || '').toString().length > 255) |
|
) { |
|
column.uidt = UITypes.LongText |
|
} else { |
|
const vals = rows.slice(columnNameRowExist ? 1 : 0).map(r => r[col]).filter(v => v !== null && v !== undefined && v.toString().trim() !== '') |
|
|
|
const checkboxType = isCheckboxType(vals) |
|
if (checkboxType.length === 1) { |
|
column.uidt = UITypes.Checkbox |
|
} else { |
|
// todo: optimize |
|
// check column is multi or single select by comparing unique values |
|
// todo: |
|
if (vals.some(v => v && v.toString().includes(','))) { |
|
let flattenedVals = vals.flatMap(v => v ? v.toString().trim().split(/\s*,\s*/) : []) |
|
const uniqueVals = flattenedVals = flattenedVals |
|
.filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase())) |
|
if (flattenedVals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(flattenedVals.length / 2)) { |
|
column.uidt = UITypes.MultiSelect |
|
column.dtxp = `'${uniqueVals.join("','")}'` |
|
} |
|
} else { |
|
const uniqueVals = vals.map(v => v.toString().trim()).filter((v, i, arr) => i === arr.findIndex(v1 => v.toLowerCase() === v1.toLowerCase())) |
|
if (vals.length > uniqueVals.length && uniqueVals.length <= Math.ceil(vals.length / 2)) { |
|
column.uidt = UITypes.SingleSelect |
|
column.dtxp = `'${uniqueVals.join("','")}'` |
|
} |
|
} |
|
} |
|
} |
|
} else if (column.uidt === UITypes.Number) { |
|
if (rows.slice(1, this.config.maxRowsToParse).some((v) => { |
|
return v && v[col] && parseInt(+v[col]) !== +v[col] |
|
})) { |
|
column.uidt = UITypes.Decimal |
|
} |
|
if (rows.slice(1, this.config.maxRowsToParse).every((v, i) => { |
|
const cellId = XLSX.utils.encode_cell({ |
|
c: range.s.c + col, |
|
r: i + (columnNameRowExist) |
|
}) |
|
|
|
const cellObj = ws[cellId] |
|
|
|
return !cellObj || (cellObj.w && cellObj.w.startsWith('$')) |
|
})) { |
|
column.uidt = UITypes.Currency |
|
} |
|
} else if (column.uidt === UITypes.DateTime) { |
|
if (rows.slice(1, this.config.maxRowsToParse).every((v, i) => { |
|
const cellId = XLSX.utils.encode_cell({ |
|
c: range.s.c + col, |
|
r: i + columnNameRowExist |
|
}) |
|
|
|
const cellObj = ws[cellId] |
|
return !cellObj || (cellObj.w && cellObj.w.split(' ').length === 1) |
|
})) { |
|
column.uidt = UITypes.Date |
|
} |
|
} |
|
} |
|
|
|
let rowIndex = 0 |
|
for (const row of rows.slice(1)) { |
|
const rowData = {} |
|
for (let i = 0; i < table.columns.length; i++) { |
|
if (table.columns[i].uidt === UITypes.Checkbox) { |
|
rowData[table.columns[i].column_name] = getCheckboxValue(row[i]) |
|
} else if (table.columns[i].uidt === UITypes.Currency) { |
|
const cellId = XLSX.utils.encode_cell({ |
|
c: range.s.c + i, |
|
r: rowIndex + columnNameRowExist |
|
}) |
|
|
|
const cellObj = ws[cellId] |
|
rowData[table.columns[i].column_name] = (cellObj && cellObj.w && cellObj.w.replace(/[^\d.]+/g, '')) || row[i] |
|
} else if (table.columns[i].uidt === UITypes.SingleSelect || table.columns[i].uidt === UITypes.MultiSelect) { |
|
rowData[table.columns[i].column_name] = (row[i] || '').toString().trim() || null |
|
} else { |
|
// toto: do parsing if necessary based on type |
|
rowData[table.columns[i].column_name] = row[i] |
|
} |
|
} |
|
this.data[tn].push(rowData) |
|
rowIndex++ |
|
} |
|
this.project.tables.push(table) |
|
} |
|
} |
|
|
|
getTemplate() { |
|
return this.project |
|
} |
|
|
|
getData() { |
|
return this.data |
|
} |
|
}
|
|
|