Merge pull request #15233 from woocommerce/feature/product-csv-import-done-step

Feature/product csv import done step
This commit is contained in:
Mike Jolley 2017-05-23 19:47:44 +01:00 committed by GitHub
commit 54cc0b34b3
12 changed files with 246 additions and 72 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5487,6 +5487,7 @@ table.bar_chart {
border-top: 1px solid #eee;
margin: 0;
padding: 23px 24px 24px;
line-height: 3em;
.button {
float: right;
@ -5512,6 +5513,10 @@ table.bar_chart {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 0 #a36597;
}
}
.woocommerce-importer-toggle-advanced-options {
color: #999;
}
}
.woocommerce-exporter,
@ -5583,11 +5588,42 @@ table.bar_chart {
height: auto;
margin: 0;
}
.woocommerce-importer-file-url-field-wrapper {
border: 1px solid #ddd;
-webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
box-shadow: inset 0 1px 2px rgba(0,0,0,.07);
background-color: #fff;
color: #32373c;
outline: 0;
line-height: 1;
display: block;
code {
background: none;
font-size: smaller;
padding: 0;
margin: 0;
color: #999;
padding: 7px 0 0 7px;
display: inline-block;
}
input {
font-family: Consolas,Monaco,monospace;
border: 0;
margin: 0;
outline: 0;
box-shadow: none;
display: inline-block;
min-width: 25%;
box-sizing: borde
}
}
}
.woocommerce-exporter-options th,
.woocommerce-importer-options th {
width: 40%;
width: 35%;
padding-right: 20px;
}
@ -5634,11 +5670,13 @@ table.bar_chart {
}
}
.wc-importer-mapping-table-wrapper {
.wc-importer-mapping-table-wrapper,
.wc-importer-error-log {
padding: 0;
}
.wc-importer-mapping-table {
.wc-importer-mapping-table,
.wc-importer-error-log-table {
margin: 0;
border: 0;
box-shadow: none;
@ -5653,7 +5691,8 @@ table.bar_chart {
}
}
tr:nth-child(odd) td {
tbody tr:nth-child(odd) td,
tbody tr:nth-child(odd) th {
background: #fbfbfb;
}
@ -5689,6 +5728,7 @@ table.bar_chart {
text-align: center;
padding: 48px 24px;
font-size: 1.5em;
line-height: 1.75em;
&::before {
@include icon( '\e015' );

View File

@ -10,12 +10,14 @@
this.mapping = wc_product_import_params.mapping;
this.position = 0;
this.file = wc_product_import_params.file;
this.skip_existing = wc_product_import_params.skip_existing;
this.update_existing = wc_product_import_params.update_existing;
this.security = wc_product_import_params.import_nonce;
// Number of import successes/failures.
this.imported = 0;
this.failed = 0;
this.updated = 0;
this.skipped = 0;
// Initial state.
this.$form.find('.woocommerce-importer-progress').val( 0 );
@ -40,7 +42,7 @@
position : $this.position,
mapping : $this.mapping,
file : $this.file,
skip_existing : $this.skip_existing,
update_existing : $this.update_existing,
security : $this.security
},
dataType: 'json',
@ -49,10 +51,12 @@
$this.position = response.data.position;
$this.imported += response.data.imported;
$this.failed += response.data.failed;
$this.updated += response.data.updated;
$this.skipped += response.data.skipped;
$this.$form.find('.woocommerce-importer-progress').val( response.data.percentage );
if ( 'done' === response.data.position ) {
window.location = response.data.url + '&imported=' + parseInt( $this.imported, 10 ) + '&failed=' + parseInt( $this.failed, 10 );
window.location = response.data.url + '&products-imported=' + parseInt( $this.imported, 10 ) + '&products-failed=' + parseInt( $this.failed, 10 ) + '&products-updated=' + parseInt( $this.updated, 10 ) + '&products-skipped=' + parseInt( $this.skipped, 10 );
} else {
$this.run_import();
}

View File

@ -1 +1 @@
!function(a,b){var c=function(a){this.$form=a,this.xhr=!1,this.mapping=wc_product_import_params.mapping,this.position=0,this.file=wc_product_import_params.file,this.skip_existing=wc_product_import_params.skip_existing,this.security=wc_product_import_params.import_nonce,this.imported=0,this.failed=0,this.$form.find(".woocommerce-importer-progress").val(0),this.run_import=this.run_import.bind(this),this.run_import()};c.prototype.run_import=function(){var c=this;a.ajax({type:"POST",url:ajaxurl,data:{action:"woocommerce_do_ajax_product_import",position:c.position,mapping:c.mapping,file:c.file,skip_existing:c.skip_existing,security:c.security},dataType:"json",success:function(a){a.success&&(c.position=a.data.position,c.imported+=a.data.imported,c.failed+=a.data.failed,c.$form.find(".woocommerce-importer-progress").val(a.data.percentage),"done"===a.data.position?b.location=a.data.url+"&imported="+parseInt(c.imported,10)+"&failed="+parseInt(c.failed,10):c.run_import())}}).fail(function(a){b.console.log(a)})},a.fn.wc_product_importer=function(){return new c(this),this},a(".woocommerce-importer").wc_product_importer()}(jQuery,window);
!function(a,b){var c=function(a){this.$form=a,this.xhr=!1,this.mapping=wc_product_import_params.mapping,this.position=0,this.file=wc_product_import_params.file,this.update_existing=wc_product_import_params.update_existing,this.security=wc_product_import_params.import_nonce,this.imported=0,this.failed=0,this.updated=0,this.skipped=0,this.$form.find(".woocommerce-importer-progress").val(0),this.run_import=this.run_import.bind(this),this.run_import()};c.prototype.run_import=function(){var c=this;a.ajax({type:"POST",url:ajaxurl,data:{action:"woocommerce_do_ajax_product_import",position:c.position,mapping:c.mapping,file:c.file,update_existing:c.update_existing,security:c.security},dataType:"json",success:function(a){a.success&&(c.position=a.data.position,c.imported+=a.data.imported,c.failed+=a.data.failed,c.updated+=a.data.updated,c.skipped+=a.data.skipped,c.$form.find(".woocommerce-importer-progress").val(a.data.percentage),"done"===a.data.position?b.location=a.data.url+"&products-imported="+parseInt(c.imported,10)+"&products-failed="+parseInt(c.failed,10)+"&products-updated="+parseInt(c.updated,10)+"&products-skipped="+parseInt(c.skipped,10):c.run_import())}}).fail(function(a){b.console.log(a)})},a.fn.wc_product_importer=function(){return new c(this),this},a(".woocommerce-importer").wc_product_importer()}(jQuery,window);

View File

@ -205,22 +205,34 @@ class WC_Admin_Importers {
$params = array(
'start_pos' => isset( $_POST['position'] ) ? absint( $_POST['position'] ) : 0,
'mapping' => isset( $_POST['mapping'] ) ? (array) $_POST['mapping'] : array(),
'skip_existing' => isset( $_POST['skip_existing'] ) ? (bool) $_POST['skip_existing'] : false,
'update_existing' => isset( $_POST['update_existing'] ) ? (bool) $_POST['update_existing'] : false,
'lines' => apply_filters( 'woocommerce_product_import_batch_size', 10 ),
'parse' => true,
);
// Log failures.
if ( 0 !== $params['start_pos'] ) {
$error_log = array_filter( (array) get_user_option( 'product_import_error_log' ) );
} else {
$error_log = array();
}
$importer = WC_Product_CSV_Importer_Controller::get_importer( $file, $params );
$results = $importer->import();
$percent_complete = $importer->get_percent_complete();
$error_log = array_merge( $error_log, $results['failed'], $results['skipped'] );
if ( 100 == $percent_complete ) {
update_user_option( get_current_user_id(), 'product_import_error_log', $error_log );
if ( 100 === $percent_complete ) {
wp_send_json_success( array(
'position' => 'done',
'percentage' => 100,
'url' => add_query_arg( array( 'nonce' => wp_create_nonce( 'product-csv' ) ), admin_url( 'edit.php?post_type=product&page=product_importer&step=done' ) ),
'imported' => count( $results['imported'] ),
'failed' => count( $results['failed'] ),
'updated' => count( $results['updated'] ),
'skipped' => count( $results['skipped'] ),
) );
} else {
wp_send_json_success( array(
@ -228,6 +240,8 @@ class WC_Admin_Importers {
'percentage' => $percent_complete,
'imported' => count( $results['imported'] ),
'failed' => count( $results['failed'] ),
'updated' => count( $results['updated'] ),
'skipped' => count( $results['skipped'] ),
) );
}
}

View File

@ -57,7 +57,7 @@ class WC_Product_CSV_Importer_Controller {
*
* @var bool
*/
protected $skip_existing = false;
protected $update_existing = false;
/**
* Get importer instance.
@ -99,7 +99,7 @@ class WC_Product_CSV_Importer_Controller {
);
$this->step = isset( $_REQUEST['step'] ) ? sanitize_key( $_REQUEST['step'] ) : current( array_keys( $this->steps ) );
$this->file = isset( $_REQUEST['file'] ) ? wc_clean( $_REQUEST['file'] ) : '';
$this->skip_existing = isset( $_REQUEST['skip_existing'] ) ? (bool) $_REQUEST['skip_existing'] : false;
$this->update_existing = isset( $_REQUEST['update_existing'] ) ? (bool) $_REQUEST['update_existing'] : false;
}
/**
@ -130,7 +130,7 @@ class WC_Product_CSV_Importer_Controller {
'step' => $keys[ $step_index + 1 ],
'file' => $this->file,
'delimiter' => $this->delimiter,
'skip_existing' => $this->skip_existing,
'update_existing' => $this->update_existing,
'_wpnonce' => wp_create_nonce( 'woocommerce-csv-importer' ), // wp_nonce_url() escapes & to & breaking redirects.
);
@ -312,7 +312,7 @@ class WC_Product_CSV_Importer_Controller {
'import_nonce' => wp_create_nonce( 'wc-product-import' ),
'mapping' => $mapping,
'file' => $this->file,
'skip_existing' => $this->skip_existing,
'update_existing' => $this->update_existing,
) );
wp_enqueue_script( 'wc-product-import' );
@ -324,8 +324,11 @@ class WC_Product_CSV_Importer_Controller {
* @todo Make this better.
*/
protected function done() {
$imported = isset( $_GET['imported'] ) ? absint( $_GET['imported'] ) : 0;
$failed = isset( $_GET['failed'] ) ? absint( $_GET['failed'] ) : 0;
$imported = isset( $_GET['products-imported'] ) ? absint( $_GET['products-imported'] ) : 0;
$updated = isset( $_GET['products-updated'] ) ? absint( $_GET['products-updated'] ) : 0;
$failed = isset( $_GET['products-failed'] ) ? absint( $_GET['products-failed'] ) : 0;
$skipped = isset( $_GET['products-skipped'] ) ? absint( $_GET['products-skipped'] ) : 0;
$errors = array_filter( (array) get_user_option( 'product_import_error_log' ) );
include_once( dirname( __FILE__ ) . '/views/html-csv-import-done.php' );
}

View File

@ -9,25 +9,84 @@ if ( ! defined( 'ABSPATH' ) ) {
<div class="wc-progress-form-content woocommerce-importer">
<section class="woocommerce-importer-done">
<?php
$results = sprintf(
$results = array();
if ( 0 < $imported ) {
$results[] = sprintf(
/* translators: %d: products count */
_n( 'Imported %s product.', 'Imported %s products.', $imported, 'woocommerce' ),
_n( '%s product imported', '%s products imported', $imported, 'woocommerce' ),
'<strong>' . number_format_i18n( $imported ) . '</strong>'
);
}
// @todo create a view to display errors or log with WC_Logger.
if ( 0 < $failed ) {
$results .= ' ' . sprintf(
if ( 0 < $updated ) {
$results[] = sprintf(
/* translators: %d: products count */
_n( 'Failed %s product.', 'Failed %s products.', $failed, 'woocommerce' ),
_n( '%s product updated', '%s products updated', $updated, 'woocommerce' ),
'<strong>' . number_format_i18n( $updated ) . '</strong>'
);
}
if ( 0 < $skipped ) {
$results[] = sprintf(
/* translators: %d: products count */
_n( '%s product was skipped', '%s products were skipped', $skipped, 'woocommerce' ),
'<strong>' . number_format_i18n( $skipped ) . '</strong>'
);
}
if ( 0 < $failed ) {
$results [] = sprintf(
/* translators: %d: products count */
_n( 'Failed to import %s product.', 'Failed to import %s products.', $failed, 'woocommerce' ),
'<strong>' . number_format_i18n( $failed ) . '</strong>'
);
}
if ( 0 < $failed || 0 < $skipped ) {
$results[] = '<a href="#" class="woocommerce-importer-done-view-errors">' . __( 'View import log', 'woocommerce' ) . '</a>';
}
/* translators: %d: import results */
echo wp_kses_post( sprintf( __( 'Import complete: %s', 'woocommerce' ), $results ) );
echo wp_kses_post( __( 'Import complete!', 'woocommerce' ) . ' ' . implode( '. ', $results ) );
?>
</section>
<section class="wc-importer-error-log" style="display:none">
<table class="widefat wc-importer-error-log-table">
<thead>
<tr>
<th><?php esc_html_e( 'Product', 'woocommerce' ); ?></th>
<th><?php esc_html_e( 'Reason for failure', 'woocommerce' ); ?></th>
</tr>
</thead>
<tbody>
<?php
if ( count( $errors ) ) {
foreach ( $errors as $error ) {
if ( ! is_wp_error( $error ) ) {
continue;
}
$error_data = $error->get_error_data();
?>
<tr>
<th><code><?php echo esc_html( $error_data['row'] ); ?></code></th>
<td><?php echo esc_html( $error->get_error_message() ); ?></td>
</tr>
<?php
}
}
?>
</tbody>
</table>
</section>
<script type="text/javascript">
jQuery(function() {
jQuery( '.woocommerce-importer-done-view-errors' ).on( 'click', function() {
jQuery( '.wc-importer-error-log' ).slideToggle();
return false;
} );
} );
</script>
<div class="wc-actions">
<a class="button button-primary" href="<?php echo esc_url( admin_url( 'edit.php?post_type=product' ) ); ?>"><?php esc_html_e( 'View products', 'woocommerce' ); ?></a>
</div>

View File

@ -32,6 +32,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<td class="wc-importer-mapping-table-field">
<select name="map_to[<?php echo esc_attr( $name ); ?>]">
<option value=""><?php esc_html_e( 'Do not import', 'woocommerce' ); ?></option>
<option value="">--------------</option>
<?php foreach ( $this->get_mapping_options( $mapped_value ) as $key => $value ) : ?>
<?php if ( is_array( $value ) ) : ?>
<optgroup label="<?php echo esc_attr( $value['name'] ); ?>">
@ -54,7 +55,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<input type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Run the importer', 'woocommerce' ); ?>" name="save_step" />
<input type="hidden" name="file" value="<?php echo esc_attr( $this->file ); ?>" />
<input type="hidden" name="delimiter" value="<?php echo esc_attr( $this->delimiter ); ?>" />
<input type="hidden" name="skip_existing" value="<?php echo (int) $this->skip_existing; ?>" />
<input type="hidden" name="update_existing" value="<?php echo (int) $this->update_existing; ?>" />
<?php wp_nonce_field( 'woocommerce-csv-importer' ); ?>
</div>
</form>

View File

@ -17,7 +17,7 @@ if ( ! defined( 'ABSPATH' ) ) {
<tr>
<th scope="row">
<label for="upload">
<?php _e( 'Choose a file from your computer:', 'woocommerce' ); ?>
<?php _e( 'Choose a CSV file from your computer:', 'woocommerce' ); ?>
</label>
</th>
<td>
@ -45,28 +45,47 @@ if ( ! defined( 'ABSPATH' ) ) {
</td>
</tr>
<tr>
<th><label for="woocommerce-importer-update-existing"><?php _e( 'Update existing products', 'woocommerce' ); ?></label><br/></th>
<td>
<input type="hidden" name="update_existing" value="0" />
<input type="checkbox" id="woocommerce-importer-update-existing" name="update_existing" value="1" />
<label for="woocommerce-importer-update-existing"><?php esc_html_e( 'If a product being imported matches an existing product ID or SKU, update the existing product data.', 'woocommerce' ); ?></label>
</td>
</tr>
<tr class="woocommerce-importer-advanced hidden">
<th>
<label for="file_url"><?php _e( 'OR enter the path to file on your server:', 'woocommerce' ); ?></label>
<label for="woocommerce-importer-file-url"><?php _e( '<em>or</em> enter the path to a CSV file on your server:', 'woocommerce' ); ?></label>
</th>
<td>
<code><?php echo esc_html( ABSPATH ) . ' '; ?></code><input type="text" id="file_url" name="file_url" size="25" />
<label for="woocommerce-importer-file-url" class="woocommerce-importer-file-url-field-wrapper">
<code><?php echo esc_html( ABSPATH ) . ' '; ?></code><input type="text" id="woocommerce-importer-file-url" name="file_url" />
</label>
</td>
</tr>
<tr>
<tr class="woocommerce-importer-advanced hidden">
<th><label><?php _e( 'CSV Delimiter', 'woocommerce' ); ?></label><br/></th>
<td><input type="text" name="delimiter" placeholder="," size="2" /></td>
</tr>
<tr>
<th><label><?php _e( 'Skip existing products', 'woocommerce' ); ?></label><br/></th>
<td>
<input type="hidden" name="skip_existing" value="0" />
<input type="checkbox" name="skip_existing" value="1" />
</td>
</tr>
</tbody>
</table>
</section>
<script type="text/javascript">
jQuery(function() {
jQuery( '.woocommerce-importer-toggle-advanced-options' ).on( 'click', function() {
var elements = jQuery( '.woocommerce-importer-advanced' );
if ( elements.is( '.hidden' ) ) {
elements.removeClass( 'hidden' );
jQuery( this ).text( jQuery( this ).data( 'hidetext' ) );
} else {
elements.addClass( 'hidden' );
jQuery( this ).text( jQuery( this ).data( 'showtext' ) );
}
return false;
} );
});
</script>
<div class="wc-actions">
<a href="#" class="woocommerce-importer-toggle-advanced-options" data-hidetext="<?php esc_html_e( 'Hide advanced options', 'woocommerce' ); ?>" data-showtext="<?php esc_html_e( 'Hide advanced options', 'woocommerce' ); ?>"><?php esc_html_e( 'Show advanced options', 'woocommerce' ); ?></a>
<input type="submit" class="button button-primary button-next" value="<?php esc_attr_e( 'Continue', 'woocommerce' ); ?>" name="save_step" />
<?php wp_nonce_field( 'woocommerce-csv-importer' ); ?>
</div>

View File

@ -128,7 +128,7 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
return 0;
}
return min( round( ( $this->file_position / $size ) * 100 ), 100 );
return absint( min( round( ( $this->file_position / $size ) * 100 ), 100 ) );
}
/**
@ -196,7 +196,7 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
} elseif ( in_array( $type, array_keys( wc_get_product_types() ), true ) || 'variation' === $type ) {
$product_type = $type;
} else {
return new WP_Error( 'woocommerce_product_csv_importer_invalid_type', __( 'Invalid product type.', 'woocommerce' ), array( 'status' => 401 ) );
return new WP_Error( 'woocommerce_product_csv_importer_invalid_type', sprintf( __( 'Invalid product type %s.', 'woocommerce' ), $type ), array( 'type' => $type, 'status' => 401 ) );
}
}
@ -209,6 +209,10 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
$product = new $classname( $id );
} elseif ( isset( $data['id'] ) ) {
$product = wc_get_product( $id );
if ( ! $product ) {
return new WP_Error( 'woocommerce_product_csv_importer_invalid_id', sprintf( __( 'Invalid product ID %d.', 'woocommerce' ), $id ), array( 'id' => $id, 'status' => 401 ) );
}
} else {
$product = new WC_Product_Simple();
}

View File

@ -36,7 +36,7 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
'lines' => -1, // Max lines to read.
'mapping' => array(), // Column mapping. csv_heading => schema_heading.
'parse' => false, // Whether to sanitize and format data.
'skip_existing' => false, // Whether to skip existing items.
'update_existing' => false, // Whether to update existing items.
'delimiter' => ',', // CSV delimiter.
);
@ -259,6 +259,31 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
}
}
/**
* Get a string to identify the row from parsed data.
*
* @param array $parsed_data
* @return string
*/
protected function get_row_id( $parsed_data ) {
$id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
$sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : '';
$name = isset( $parsed_data['name'] ) ? esc_attr( $parsed_data['name'] ) : '';
$row_data = array();
if ( $name ) {
$row_data[] = $name;
}
if ( $id ) {
$row_data[] = __( 'ID: ', 'woocommerce' ) . $id;
}
if ( $sku ) {
$row_data[] = __( 'SKU: ', 'woocommerce' ) . $sku;
}
return implode( ', ', $row_data );
}
/**
* Process importer.
*
@ -268,20 +293,22 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
$data = array(
'imported' => array(),
'failed' => array(),
'updated' => array(),
'skipped' => array(),
);
foreach ( $this->parsed_data as $parsed_data ) {
foreach ( $this->parsed_data as $parsed_data_key => $parsed_data ) {
// Don't import products with IDs or SKUs that already exist if option is true.
if ( $this->params['skip_existing'] ) {
if ( ! $this->params['update_existing'] ) {
$id = isset( $parsed_data['id'] ) ? absint( $parsed_data['id'] ) : 0;
$sku = isset( $parsed_data['sku'] ) ? esc_attr( $parsed_data['sku'] ) : '';
if ( $id && wc_get_product( $id ) ) {
$data['failed'][] = new WP_Error( 'woocommerce_product_csv_importer_error', __( 'A product with this ID already exists.', 'woocommerce' ), array( 'id' => $id ) );
$data['skipped'][] = new WP_Error( 'woocommerce_product_csv_importer_error', __( 'A product with this ID already exists.', 'woocommerce' ), array( 'id' => $id, 'row' => $this->get_row_id( $parsed_data ) ) );
continue;
} elseif ( $sku && wc_get_product_id_by_sku( $sku ) ) {
$data['failed'][] = new WP_Error( 'woocommerce_product_csv_importer_error', __( 'A product with this SKU already exists.', 'woocommerce' ), array( 'sku' => $sku ) );
$data['skipped'][] = new WP_Error( 'woocommerce_product_csv_importer_error', __( 'A product with this SKU already exists.', 'woocommerce' ), array( 'sku' => $sku, 'row' => $this->get_row_id( $parsed_data ) ) );
continue;
}
}
@ -289,7 +316,10 @@ class WC_Product_CSV_Importer extends WC_Product_Importer {
$result = $this->process_item( $parsed_data );
if ( is_wp_error( $result ) ) {
$result->add_data( array( 'row' => $this->get_row_id( $parsed_data ) ) );
$data['failed'][] = $result;
} elseif ( ! empty( $parsed_data['id'] ) ) {
$data['updated'][] = $result;
} else {
$data['imported'][] = $result;
}