fix: Column Mapping

- Remove double headers
- Indicate match with indicators
- Column Mapping Dialog
- Handle untitled columns
This commit is contained in:
Faris Ansari 2019-09-24 14:12:27 +05:30
parent e09b45ed81
commit 1bfd7d3731
4 changed files with 143 additions and 118 deletions

View file

@ -9,6 +9,7 @@ import timeit
import frappe
from datetime import datetime
from frappe import _
from frappe.core.doctype.docfield.docfield import DocField
from frappe.utils import cint, flt, DATE_FORMAT, DATETIME_FORMAT
from frappe.utils.csvutils import read_csv_content
from frappe.utils.xlsxutils import (
@ -119,6 +120,23 @@ class Importer:
def get_data_for_import_preview(self):
out = self.get_parsed_data_from_template()
# prepare fields
fields = []
for df in out.fields:
header_title = df.header_title
skip_import = df.skip_import
if isinstance(df, DocField):
field = df.as_dict()
else:
field = df
field.update({
'header_title': header_title,
'skip_import': skip_import
})
fields.append(field)
out.fields = fields
if len(out.data) > MAX_ROWS_IN_PREVIEW:
out.data = []
out.max_rows_exceeded = True
@ -146,30 +164,37 @@ class Importer:
df_by_labels_and_fieldnames = self.build_fields_dict_for_column_matching()
for i, value in enumerate(self.header_row):
for i, header_title in enumerate(self.header_row):
header_row_index = str(i)
if remap_column.get(header_row_index):
column_name = value
value = remap_column.get(header_row_index)
fieldname = remap_column.get(header_row_index)
df = df_by_labels_and_fieldnames.get(fieldname)
warnings.append(
_("Column {0}: Mapping column {1} to field {2}").format(
i, frappe.bold(column_name), frappe.bold(value)
i, frappe.bold(header_title or '<i>Untitled Column</i>'), frappe.bold(df.label)
)
)
else:
df = df_by_labels_and_fieldnames.get(header_title)
field = df_by_labels_and_fieldnames.get(value)
if not field or i in skip_import:
field = frappe._dict({"label": value, "skip_import": True})
if value and i not in skip_import:
warnings.append(
_("Column {0}: Cannot match column {1} with any field").format(
i, frappe.bold(value)
)
if not df:
field = frappe._dict(header_title=header_title, skip_import=True)
else:
field = df
field.header_title = header_title
field.skip_import = False
if i in skip_import:
field.skip_import = True
warnings.append(_("Column {0}: Skipping column {1}").format(i, frappe.bold(header_title)))
elif header_title and not df:
warnings.append(
_("Column {0}: Cannot match column {1} with any field").format(
i, frappe.bold(header_title)
)
elif i in skip_import:
warnings.append(_("Column {0}: Skipping column {1}").format(i, frappe.bold(value)))
else:
warnings.append(_("Column {0}: Skipping untitled column").format(i))
)
elif not header_title and not df:
warnings.append(_("Column {0}: Skipping untitled column").format(i))
fields.append(field)
return fields, warnings
@ -442,7 +467,9 @@ class Importer:
# set status
failures = [l for l in import_log if l.get("success") == False]
if len(failures) > 0:
if len(failures) == total_payload_count:
status = "Pending"
elif len(failures) > 0:
status = "Partial Success"
else:
status = "Success"

View file

@ -59,10 +59,13 @@ frappe.ui.form.on('Data Import Beta', {
frm.trigger('show_import_log');
frm.trigger('toggle_submit_after_import');
if (frm.doc.status === 'Success') {
if (frm.doc.import_log && frm.doc.import_log !== '[]') {
// set form as readonly
frm.fields.forEach(f => f.df.read_only = 1);
frm.disable_save();
}
if (frm.doc.status === 'Success') {
frm.events.show_success_message(frm);
} else {
if (!frm.is_new() && frm.doc.import_file) {
@ -144,8 +147,6 @@ frappe.ui.form.on('Data Import Beta', {
.call({
doc: frm.doc,
method: 'get_preview_from_template',
freeze: true,
freeze_message: __('Preparing Preview...'),
error_handlers: {
TimestampMismatchError() {
// ignore this error
@ -179,23 +180,19 @@ frappe.ui.form.on('Data Import Beta', {
import_log,
warnings,
events: {
remap_column(header_row_index, fieldname) {
remap_column(changed_map) {
let template_options = JSON.parse(frm.doc.template_options || '{}');
template_options.remap_column = template_options.remap_column || {};
template_options.remap_column[header_row_index] = fieldname;
Object.assign(template_options.remap_column, changed_map);
// if the column is remapped, remove it from skip_import
if (
template_options.skip_import &&
template_options.skip_import.includes(header_row_index)
) {
if (template_options.skip_import) {
template_options.skip_import = template_options.skip_import.filter(
d => d !== header_row_index
d => !Object.keys(template_options.remap_column).includes(cstr(d))
);
}
frm.set_value('template_options', JSON.stringify(template_options));
frm.save().then(() => {
frm.trigger('import_file');
});
frm.save().then(() => frm.trigger('import_file'));
},
skip_import(header_row_index) {

View file

@ -1,38 +0,0 @@
import ColumnManager from 'frappe-datatable/src/columnmanager';
export default function(header_row) {
return class CustomColumnManager extends ColumnManager {
getHeaderHTML(columns) {
let html = super.getHeaderHTML(columns);
let header_row_columns = [
{
id: '_checkbox',
colIndex: 0,
format: () => ''
}
// {
// id: 'Sr. No',
// colIndex: 1,
// format: () => ''
// }
].concat(
...header_row.map((col, i) => {
return {
id: col,
name: col,
align: 'left',
dropdown: false,
content: col,
colIndex: i + 1
};
})
);
let header_row_html = this.rowmanager.getRowHTML(header_row_columns, {
rowIndex: 'header-row'
});
return header_row_html + html;
}
};
}

View file

@ -1,5 +1,4 @@
import DataTable from 'frappe-datatable';
import get_custom_column_manager from './custom_column_manager';
import ColumnPickerFields from './column_picker_fields';
frappe.provide('frappe.data_import');
@ -47,7 +46,11 @@ frappe.data_import.ImportPreview = class ImportPreview {
<div>
<div class="warnings text-muted"></div>
<div class="table-preview"></div>
<div class="table-actions margin-top"></div>
<div class="table-actions margin-top">
<button class="btn btn-sm btn-default" data-action="show_column_mapper">
${__('Map Columns')}
</button>
</div>
</div>
`);
frappe.utils.bind_actions_with_class(this.wrapper, this);
@ -61,18 +64,23 @@ frappe.data_import.ImportPreview = class ImportPreview {
this.columns = this.fields.map((df, i) => {
let header_row_index = i - 1;
if (df.skip_import) {
let is_sr = df.label === 'Sr. No';
let column_title = is_sr
? df.label
: `<span class="indicator red">${df.header_title || `<i>${__('Untitled Column')}</i>`}</span>`;
return {
id: frappe.utils.get_random(6),
name: df.label,
content: column_title,
skip_import: true,
editable: false,
focusable: false,
align: 'left',
header_row_index,
width: df.label === 'Sr. No' ? 60 : column_width,
width: is_sr ? 60 : column_width,
format: (value, row, column, data) => {
let html = `<div class="text-muted">${value}</div>`;
if (df.label === 'Sr. No' && this.is_row_imported(row)) {
if (is_sr && this.is_row_imported(row)) {
html = `
<div class="flex justify-between">${SVG_ICONS['checkbox-circle-line'] +
html}</div>
@ -94,6 +102,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
return {
id: df.fieldname,
name: column_title,
content: `<span class="indicator green">${df.header_title || df.label}</span>`,
df: df,
editable: true,
align: 'left',
@ -146,20 +155,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
serialNoColumn: false,
checkboxColumn: false,
pasteFromClipboard: true,
noDataMessage: no_data_message,
headerDropdown: [
{
label: __('Remap Column'),
action: col => this.remap_column(col)
},
{
label: __('Skip Import'),
action: col => this.skip_import(col)
}
],
overrideComponents: {
ColumnManager: get_custom_column_manager(this.header_row)
}
noDataMessage: no_data_message
});
if (this.data.length === 0) {
@ -168,7 +164,7 @@ frappe.data_import.ImportPreview = class ImportPreview {
});
}
this.datatable.style.setStyle('.dt-dropdown__list-item:nth-child(-n+4)', {
this.datatable.style.setStyle('.dt-dropdown', {
display: 'none'
});
}
@ -180,35 +176,6 @@ frappe.data_import.ImportPreview = class ImportPreview {
}
setup_styles() {
let columns = this.datatable.getColumns();
columns.forEach(col => {
let class_name = [
`.dt-header .dt-cell--col-${col.colIndex}`,
`.dt-header .dt-cell--col-${col.colIndex} .dt-dropdown__toggle`
].join(',');
if (!col.skip_import && col.df) {
this.datatable.style.setStyle(class_name, {
backgroundColor: frappe.ui.color.get_color_shade(
'green',
'extra-light'
),
color: frappe.ui.color.get_color_shade('green', 'dark')
});
}
if (col.skip_import && col.name !== 'Sr. No') {
this.datatable.style.setStyle(class_name, {
backgroundColor: frappe.ui.color.get_color_shade(
'orange',
'extra-light'
),
color: frappe.ui.color.get_color_shade('orange', 'dark')
});
this.datatable.style.setStyle(`.dt-cell--col-${col.colIndex}`, {
backgroundColor: frappe.ui.color.get_color_shade('white', 'light')
});
}
});
// import success checkbox
this.datatable.style.setStyle(`svg.import-success`, {
width: '16px',
@ -232,8 +199,8 @@ frappe.data_import.ImportPreview = class ImportPreview {
let failures = this.import_log.filter(log => !log.success);
if (failures.length > 0) {
this.wrapper.find('.table-actions').append(
`<button class="btn btn-xs btn-default" data-action="export_errored_rows">
${__('Export rows which are not imported')}
`<button class="btn btn-sm btn-default" data-action="export_errored_rows">
${__('Export Errored Rows')}
</button>
`);
}
@ -243,6 +210,78 @@ frappe.data_import.ImportPreview = class ImportPreview {
this.events.export_errored_rows();
}
show_column_mapper() {
let column_picker_fields = new ColumnPickerFields({
doctype: this.doctype
});
let changed = [];
let fields = this.fields.map((df, i) => {
if (df.label === 'Sr. No') return [];
let fieldname;
if (df.skip_import) {
fieldname = null;
} else {
fieldname = df.parent === this.doctype
? df.fieldname
: `${df.parent}:${df.fieldname}`;
}
return [
{
label: __('Column {0}', [i]),
fieldtype: 'Data',
default: df.header_title,
fieldname: `Column ${i}`,
read_only: 1
},
{
fieldtype: 'Button',
label: 'Skip Column',
fieldname: 'skip_' + i,
click: () => {
let header_row_index = i - 1;
this.events.skip_import(header_row_index);
}
},
{
fieldtype: 'Column Break'
},
{
fieldtype: 'Autocomplete',
fieldname: i,
label: __('Select field'),
max_items: Infinity,
options: column_picker_fields.get_fields_as_options(),
default: fieldname,
change() {
changed.push(i);
}
},
{
fieldtype: 'Section Break'
}
];
});
// flatten the array
fields = fields.reduce((acc, curr) => [...acc, ...curr]);
let dialog = new frappe.ui.Dialog({
title: __('Column Mapper'),
fields,
primary_action: (values) => {
let changed_map = {};
changed.map(i => {
let header_row_index = i - 1;
changed_map[header_row_index] = values[i];
});
if (changed.length > 0) {
this.events.remap_column(changed_map);
}
dialog.hide();
}
});
dialog.show();
}
remap_column(col) {
let column_picker_fields = new ColumnPickerFields({
doctype: this.doctype