Commit 4508e06f authored by KR Moorhouse's avatar KR Moorhouse

Merge branch 'issue#3757' into 'feat-stage-two'

New Batch Processor, New Form Importer, New Form Template Importer

See merge request ninja-forms/ninja-forms!3712
parents 64bf655b 65551d7b
Pipeline #650 passed with stage
in 1 minute and 14 seconds
......@@ -67,4 +67,8 @@
.alignleft.actions input[type=text] {
height: 28px;
border-radius: 4px;
}
.jBox-content {
overflow: hidden !important;
}
\ No newline at end of file
jQuery( document ).ready( function( $ ) {
/**
* Stores the selected file details for form imports.
*
* name is the filename from the user's computer, including extension.
* content is the base64 encoded contents as a result of using the HTML5 FileReader API.
* @type {Object}
*/
var importFormFile = {
name: '',
content: ''
};
/**
* Listen for clicks on our "import" button.
* It sets data for our batch processor and then instantiates a batch process.
*
* @since UPDATE_VERSION_ON_MERGE
* @param object e Click Event
* @return void
*/
$( document ).on( 'click', '#nf-import-form-submit', function( e ) {
// Make sure that our file field isn't empty.
if ( '' == importFormFile.name ) return false;
// Settings object for our batch processor
var settings = {
batch_type: 'import_form',
extraData: importFormFile,
loadingText: 'Importing...',
onCompleteCallback: function( response ) {
// If we don't get back a form ID, then bail.
if ( 'undefined' == typeof response.form_id ) return false;
jQuery( '#nf-import-file' ).val('');;
jQuery( '#nf-import-url' ).attr( 'href', nfAdmin.builderURL + response.form_id );
jQuery( '#row-nf-import-response' ).show();
}
}
/**
* Instantiate our batch processor.
*
* This will open the modal and present the user with content and buttons.
*/
new NinjaBatchProcessor( settings );
} );
/**
* Selecting a file within an input field triggers a jQuery change event.
*
* When we select a form file to import, we need to do a few things:
*
* Disable the primary button of our batch processing modal.
* Grab the file and make sure that it has a .nff extension.
* Read the contents and base64 encode them using the HTML5 FileReader API.
* Set the contents to our importFormFile variable.
*
* @since UPDATE_VERSION_ON_MERGE
* @param object e Change Event
* @return {[type]} [description]
*/
$( document ).on( 'change', '#nf-import-file', function( e ) {
// Hide our success message.
jQuery( '#row-nf-import-response' ).hide();
// Hide our extension type error.
jQuery( '#row-nf-import-type-error' ).hide();
// Grab the file from the input.
var file = e.target.files[0];
// If our file var is empty, bail.
if ( ! file ) {
return false;
}
// Use some Regex to get the extension
var extension = file.name.match(/\.[0-9a-z]+$/i);
// If we don't have a .nff extension, show our type error and bail.
if ( '.nff' !== extension[0] ) {
jQuery( '#row-nf-import-type-error' ).show();
importFormFile.name = '';
importFormFile.content = '';
return false;
}
// Instantiate the HTML5 FileReader API.
var reader = new FileReader();
/**
* When the HTML5 API says that we've successfully loaded the file contents:
* Set our importFormFile var.
* Enable our batch processor primary button.
* We use Javascript's addEventListener to update our var.
*/
reader.addEventListener( 'load', function () {
importFormFile.name = file.name;
importFormFile.content = reader.result;
}, false);
// Use the readAsDataURL method of the FileReader API.
reader.readAsDataURL( file );
} );
var clickedElement;
......
......@@ -68,12 +68,12 @@ jQuery(document).ready(function($) {
downgradeInput.style.width = '100%';
downgradeInput.style.height = '3em';
downgradeCTA = document.createElement( 'p' );
downgradeCTA.innerHTML = nf_settings.i18n.downgradeConfirmMessage;
downgradeCTA.innerHTML = nfAdmin.i18n.downgradeConfirmMessage;
downgradeWarning = document.createElement( 'p' );
downgradeWarning.innerHTML = nf_settings.i18n.downgradeWarningMessage;
downgradeWarning.innerHTML = nfAdmin.i18n.downgradeWarningMessage;
downgradeWarning.style.color = 'red';
downgradeTitle = document.createElement( 'h3' );
downgradeTitle.innerHTML = nf_settings.i18n.downgradeMessage;
downgradeTitle.innerHTML = nfAdmin.i18n.downgradeMessage;
downgradeContainer = document.createElement( 'div' );
downgradeContainer.appendChild( downgradeTitle );
downgradeContainer.appendChild( downgradeWarning );
......@@ -105,20 +105,20 @@ jQuery(document).ready(function($) {
var last_form = 0;
// Gives the user confidence things are happening
$( '#progressMsg' ).html( 'Deleting submissions for '
+ nf_settings.forms[ formIndex ].title + "" + ' ( ID: '
+ nf_settings.forms[ formIndex ].id + ' )' );
+ nfAdmin.forms[ formIndex ].title + "" + ' ( ID: '
+ nfAdmin.forms[ formIndex ].id + ' )' );
$( '#progressMsg').show();
// notify php this is the last one so it delete data and deactivate NF
if( formIndex === nf_settings.forms.length - 1 ) {
if( formIndex === nfAdmin.forms.length - 1 ) {
last_form = 1;
}
// do this deletion thang
$.post(
nf_settings.ajax_url,
nfAdmin.ajax_url,
{
'action': 'nf_delete_all_data',
'form': nf_settings.forms[ formIndex ].id,
'security': nf_settings.nonce,
'form': nfAdmin.forms[ formIndex ].id,
'security': nfAdmin.nonce,
'last_form': last_form
}
).then (function( response ) {
......@@ -126,7 +126,7 @@ jQuery(document).ready(function($) {
response = JSON.parse( response );
// we expect success and then move to the next form
if( response.data.success ) {
if( formIndex < nf_settings.forms.length ) {
if( formIndex < nfAdmin.forms.length ) {
doAllDataDeletions( formIndex )
} else {
// if we're finished deleting data then redirect to plugins
......@@ -139,7 +139,7 @@ jQuery(document).ready(function($) {
// writes error messages to console to help us debug
console.log( xhr.status + ' ' + error + '\r\n' +
'There was an error deleting submissions for '
+ nf_settings.forms[ formIndex ].title );
+ nfAdmin.forms[ formIndex ].title );
});
};
// Add event listener for delete button
......@@ -176,10 +176,10 @@ jQuery(document).ready(function($) {
'field': {
id: $( that ).data( 'id' )
},
'security': nf_settings.nonce
'security': nfAdmin.nonce
};
$.post( nf_settings.ajax_url, data )
$.post( nfAdmin.ajax_url, data )
.done( function( response ) {
$( that ).closest( 'tr').fadeOut().remove();
});
......@@ -194,7 +194,7 @@ jQuery(document).ready(function($) {
// TODO: Maybe this should be build using DOM node construction?
content: downgradeContainer.innerHTML,
btnPrimary: {
text: nf_settings.i18n.downgradeButtonPrimary,
text: nfAdmin.i18n.downgradeButtonPrimary,
class: 'nfDowngradeButtonPrimary',
callback: function( e ) {
// If our "Downgrade" button does not have have an attribute of disabled...
......@@ -210,7 +210,7 @@ jQuery(document).ready(function($) {
}
},
btnSecondary: {
text: nf_settings.i18n.downgradeButtonSecondary,
text: nfAdmin.i18n.downgradeButtonSecondary,
class: 'nfDowngradeButtonSecondary',
callback: function( e ) {
// Close the modal if this button is clicked.
......@@ -256,7 +256,7 @@ jQuery(document).ready(function($) {
} );
// If we're allowed to track site data...
if ( '1' == nf_settings.allow_telemetry ) {
if ( '1' == nfAdmin.allow_telemetry ) {
// Show the optout button.
$( '#nfTelOptin' ).addClass( 'hidden' );
$( '#nfTelOptout' ).removeClass( 'hidden' );
......@@ -294,72 +294,14 @@ jQuery(document).ready(function($) {
} );
jQuery( '#nfTrashExpiredSubmissions' ).click( function( e ) {
var that = this;
var data = {
closeOnClick: false,
closeOnEsc: true,
content: '<p>' + nf_settings.i18n.trashExpiredSubsMessage + '<p>',
btnPrimary: {
text: nf_settings.i18n.trashExpiredSubsButtonPrimary,
callback: function( e ) {
// Hide the buttons.
deleteModal.maybeShowActions( false );
// Show the progress bar.
deleteModal.maybeShowProgress( true );
// Begin our cleanup process.
that.submissionExpirationProcess( that, -1, deleteModal );
}
},
btnSecondary: {
text: nf_settings.i18n.trashExpiredSubsButtonSecondary,
callback: function( e ) {
deleteModal.toggleModal( false );
}
},
useProgressBar: true,
};
this.submissionExpirationProcess = function( context, steps, modal ) {
var data = {
action: 'nf_batch_process',
batch_type: 'expired_submission_cleanup',
security: nf_settings.batch_nonce
};
jQuery.post( nf_settings.ajax_url, data, function( response ) {
response = JSON.parse( response );
// If we're done...
if ( response.batch_complete ) {
// Push our progress bar to 100%.
modal.setProgress( 100 );
modal.toggleModal( false );
// Exit.
return false;
}
// If we do not yet have a determined number of steps...
if ( -1 == steps ) {
// If step_toal is defined...
if ( 'undefined' != typeof response.step_total ) {
// Use the step_total.
steps = response.step_total;
} // Otherwise... (step_total is not defined)
else {
// Use step_remaining.
steps = response.step_remaining;
}
}
// Calculate our current step.
var step = steps - response.step_remaining;
// Calculate our maximum progress for this step.
var maxProgress = Math.round( step / steps * 100 );
// Increment the progress.
modal.incrementProgress ( maxProgress );
// Recall our function...
context.submissionExpirationProcess( context, steps, modal );
} );
}
var deleteModal = new NinjaModal( data );
var settings = {
content: '<p>' + nfAdmin.i18n.trashExpiredSubsMessage + '</p>',
btnPrimaryText: nfAdmin.i18n.trashExpiredSubsButtonPrimary,
btnSecondaryText: nfAdmin.i18n.trashExpiredSubsButtonSecondary,
batch_type: 'expired_submission_cleanup',
// extraData: [ 'test1', 'test2', 'test3' ]
}
new NinjaBatchProcessor( settings );
});
jQuery( '#nfRemoveMaintenanceMode' ).click( function( e ) {
......
/**
* Batch Processor JS Object
*/
function NinjaBatchProcessor( settings ) {
var that = this;
var modalData = {
closeOnClick: false,
closeOnEsc: true,
useProgressBar: true
};
/**
* If we haven't been passed any content, make sure we pass an empty content var.
*/
if ( 'undefined' == typeof settings.content ) {
settings.content = '';
}
// Set our modalData content var.
modalData.content = settings.content;
/**
* If we've been passed a loadingText var, pass that along.
*/
if ( 'undefined' != typeof settings.loadingText ) {
modalData.loadingText = settings.loadingText;
}
/**
* If we haven't defined button text for our primary button, we don't want to pass button settings.
*
* Check to see if we've defined primary button text, and if we have, add button settings to the modalData.
*/
if ( 'undefined' != typeof settings.btnPrimaryText ) {
modalData.btnPrimary = {
text: settings.btnPrimaryText,
callback: function( e ) {
// Hide the buttons.
modalInstance.maybeShowActions( false );
// Show the progress bar.
modalInstance.maybeShowProgress( true );
// Begin our cleanup process.
that.postToProcessor( that, -1, modalInstance );
}
};
modalData.btnSecondary = {
text: settings.btnSecondaryText,
callback: function( e ) {
modalInstance.toggleModal( false );
}
};
} else { // If we don't have any buttons defined, then we want to run the batch process on modal open.
modalData.onOpenCallback = function() {
// Hide the buttons.
this.maybeShowActions( false );
// Show the progress bar.
this.maybeShowProgress( true );
// Begin our cleanup process.
that.postToProcessor( that, -1, this );
}
}
this.postToProcessor = function( context, steps, modal, data ) {
if ( 'undefined' == typeof data ) {
var data = {
action: 'nf_batch_process',
batch_type: settings.batch_type,
security: nfAdmin.batchNonce,
extraData: settings.extraData
};
}
jQuery.post( ajaxurl, data, function( response ) {
response = JSON.parse( response );
// If we're done...
if ( response.batch_complete ) {
// Push our progress bar to 100%.
modal.setProgress( 100 );
modal.toggleModal( false );
if ( 'undefined' != typeof settings.onCompleteCallback ) {
settings.onCompleteCallback( response );
}
// Exit.
return false;
}
// If we do not yet have a determined number of steps...
if ( -1 == steps ) {
// If step_toal is defined...
if ( 'undefined' != typeof response.step_total ) {
// Use the step_total.
steps = response.step_total;
} // Otherwise... (step_total is not defined)
else {
// Use step_remaining.
steps = response.step_remaining;
}
}
// If our PHP edited our extraData variable, update our JS var and pass it along.
if ( 'undefined' != typeof response.extraData ) {
// Update our extraData property.
data.extraData = response.extraData;
}
// Calculate our current step.
var step = steps - response.step_remaining;
// Calculate our maximum progress for this step.
var maxProgress = Math.round( step / steps * 100 );
// Increment the progress.
modal.incrementProgress ( maxProgress );
// Recall our function...
context.postToProcessor( context, steps, modal, data );
} );
}
var modalInstance = new NinjaModal( modalData );
}
\ No newline at end of file
......@@ -37,6 +37,7 @@ function NinjaModal ( data ) {
this.buttons.secondary = {};
this.buttons.primary.data = ( 'undefined' != typeof data.btnPrimary ? data.btnPrimary : false );
this.buttons.secondary.data = ( 'undefined' != typeof data.btnSecondary ? data.btnSecondary : false );
this.onOpenCallback = ( 'undefined' != typeof data.onOpenCallback ? data.onOpenCallback : false );
// See if we need the progress bar.
this.useProgressBar = ( 'undefined' != typeof data.useProgressBar ? data.useProgressBar : false );
if ( this.useProgressBar ) {
......@@ -170,6 +171,10 @@ NinjaModal.prototype.initModal = function () {
// Attach the callback.
jQuery( this.content ).find( '#nf-button-secondary-' + this.dataId ).click( that.buttons.secondary.callback );
}
if ( that.onOpenCallback ) {
that.onOpenCallback();
}
},
} );
// Setup our data id to keep the DOM ids unique.
......@@ -268,6 +273,7 @@ NinjaModal.prototype.renderContent = function () {
this.buttons.secondary.dom.onclick = this.buttons.secondary.callback;
}
}
// Set our content.
this.popup.setContent( document.createElement( 'div' ).appendChild( contentBox ).parentElement.innerHTML );
}
......
This diff is collapsed.
This diff is collapsed.
......@@ -21,23 +21,41 @@ define( [], function() {
* @return {void}
*/
maybeOpenModal: function( e ) {
// If this isn't an ad, then early return
if ( 'ad' != this.model.get( 'type' ) ) {
return true;
}
// Prevent page navigation.
e.preventDefault();
e.preventDefault();
// If this is an ad, open the ad modal.
if ( 'ad' == this.model.get( 'type' ) ) {
// Open our jBox modal
var modal = new jBox( 'Modal', {
width: 450,
title: this.model.get( 'modal-title' ),
content: this.model.get( 'modal-content' ),
closeButton: 'box',
blockScroll: true
} );
// Open our jBox modal
var modal = new jBox( 'Modal', {
width: 450,
title: this.model.get( 'modal-title' ),
content: this.model.get( 'modal-content' ),
closeButton: 'box',
blockScroll: true
} );
modal.open();
} else { // This is a template, so import it using the batch processor.
// Settings object for our batch processor
var settings = {
// Batch processor slug. Must match what we have set in our PHP settings array.
batch_type: 'import_form_template',
loadingText: 'Importing...',
extraData: { template: this.model.get( 'id' ) },
onCompleteCallback: function( response ) {
// Bail if we don't return a form ID.
if ( 'undefined' == typeof response.form_id ) return false;
modal.open();
window.location.href = nfAdmin.builderURL + response.form_id;
}
}
/**
* Instantiate our batch processor.
*
* This will open the modal and present the user with content.
*/
new NinjaBatchProcessor( settings );
}
}
} );
......
......@@ -22,27 +22,19 @@ class NF_AJAX_REST_BatchProcess extends NF_AJAX_REST_Controller
if ( ! isset( $request_data[ 'security' ] ) || ! wp_verify_nonce( $request_data[ 'security' ], 'ninja_forms_batch_nonce' ) ) {
// Kick the request out now.
$data[ 'error' ] = __( 'Request forbidden.', 'ninja-forms' );
return $data;
}
// If we have a batch type...
if ( isset( $request_data[ 'batch_type' ]) ){
$batch_type = $request_data[ 'batch_type' ];
// Route the request to the proper controller.
switch ( $batch_type ) {
case 'chunked_publish':
$batch = new NF_Admin_Processes_ChunkPublish(
$request_data );
break;
case 'data_cleanup':
$batch = new NF_Admin_Processes_DataCleanup(
$request_data );
break;
case 'expired_submission_cleanup':
$batch = new NF_Admin_Processes_ExpiredSubmissionCleanup(
$request_data );
break;
default:
$data[ 'error' ] = __( 'Invalid request.', 'ninja-forms' );
break;
$batch_processes = Ninja_Forms()->config( 'BatchProcesses' );
if ( isset ( $batch_processes[ $batch_type ][ 'class_name' ] ) ) {
$batch_class = $batch_processes[ $batch_type ][ 'class_name' ];
$batch = new $batch_class( $request_data );
} else {
$data[ 'error' ] = __( 'Invalid request.', 'ninja-forms' );
}
} // Otherwise... (We don't have a batch type.)
else {
......
......@@ -13,10 +13,10 @@ class NF_AJAX_REST_NewFormTemplates extends NF_AJAX_REST_Controller
$templates = Ninja_Forms()->config( 'NewFormTemplates' );
usort( $templates, array( $this, 'cmp' ) );
array_unshift( $templates, array(
'id' => 'new',
'title' => __( 'Blank Form', 'ninja-forms' ),
'id' => 'new',
'title' => __( 'Blank Form', 'ninja-forms' ),
'template-desc' => __( 'The blank form allows you to create any type of form using our drag & drop builder.', 'ninja-forms' ),
'type' => 'default'
'type' => 'default'
) );
return array_values( $templates ); // Remove keys so that the JSON is an array.
}
......
......@@ -5,6 +5,15 @@
*/
abstract class NF_Abstracts_BatchProcess
{
protected $_db;
/**
* Array that holds data we're sending back to the JS front-end.
* @var array
*/
protected $response = array(
'batch_complete' => false
);
/**
* Constructor
......@@ -14,11 +23,49 @@ abstract class NF_Abstracts_BatchProcess
//Bail if we aren't in the admin.
if ( ! is_admin() )
return false;
global $wpdb;
/**
* Set $_db to $wpdb.
* This helps us by not requiring us to declare global $wpdb in every class method.
*/
$this->_db = $wpdb;
// Run init.
$this->init();
}
/**
* Decides whether we need to run startup or restart and then calls processing.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function init()
{
if ( ! get_option( 'nf_doing_' . $this->_slug ) ) {
// Run the startup process.
$this->startup();
} else {
// Otherwise... (We've already run startup.)
$this->restart();
}
// Determine how many steps this will take.
$this->response[ 'step_total' ] = $this->get_steps();
add_option( 'nf_doing_' . $this->_slug, true );
// Run processing
$this->process();
}
/**
* Function to loop over the batch.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function process()
{
......@@ -27,9 +74,11 @@ abstract class NF_Abstracts_BatchProcess
*/
}
/**
* Function to run any setup steps necessary to begin processing.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function startup()
{
......@@ -38,9 +87,37 @@ abstract class NF_Abstracts_BatchProcess
*/
}
/**
* Function to run any setup steps necessary to begin processing for steps after the first.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function restart()
{
/**
* This function intentionally left empty.
*/
}
/**
* Returns how many steps we have in this process.
*
* If this method isn't overwritten by a child, it defaults to 1.
*
* @since UPDATE_VERSION_ON_MERGE
* @return int
*/
public function get_steps()
{
return 1;
}
/**
* Function to cleanup any lingering temporary elements of a batch process after completion.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function cleanup()
{
......@@ -49,4 +126,53 @@ abstract class NF_Abstracts_BatchProcess
*/
}
/**
* Method called when we are finished with this process.
*
* Deletes our "doing" option.
* Set's our response 'batch_complete' to true.
* Runs cleanup().
* Responds to the JS front-end.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function batch_complete()
{
// Delete our options.
delete_option( 'nf_doing_' . $this->_slug );
// Tell our JS that we're done.
$this->response[ 'batch_complete' ] = true;
$this->cleanup();
$this->respond();
}
/**
* Method that immediately moves on to the next step.
*
* Used in child methods to stop processing the current step an dmove to the next.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function next_step()
{
// ..see how many steps we have left, update our option, and send the remaining step to the JS.
$this->response[ 'step_remaining' ] = $this->get_steps();
$this->respond();
}
/**
* Method that encodes $this->response and sends the data to the front-end.
*
* @since UPDATE_VERSION_ON_MERGE
* @return void
*/
public function respond()
{