installed plugin Easy Digital Downloads version 3.1.0.3

This commit is contained in:
2022-11-27 15:03:07 +00:00
committed by Gitium
parent 555673545b
commit c5dce2cec6
1200 changed files with 238970 additions and 0 deletions

View File

@ -0,0 +1,130 @@
/**
* Internal dependencies
*/
import OrderOverview from './order-overview';
import './order-details';
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
// Order Overview.
if ( window.eddAdminOrderOverview ) {
OrderOverview.render();
/**
* Add validation to Add/Edit Order form.
*
* @since 3.0
*/
( () => {
const overview = OrderOverview.options.state;
const orderItems = overview.get( 'items' );
const noItemErrorEl = document.getElementById( 'edd-add-order-no-items-error' );
const noCustomerErrorEl = document.getElementById( 'edd-add-order-customer-error' );
const assignCustomerEl = document.getElementById( 'customer_id' );
const newCustomerEmailEl = document.getElementById( 'edd_new_customer_email' );
[
'edd-add-order-form',
'edd-edit-order-form',
].forEach( ( form ) => {
const formEl = document.getElementById( form );
if ( ! formEl ) {
return;
}
formEl.addEventListener( 'submit', submitForm );
} );
/**
* Submits an Order form.
*
* @since 3.0
*
* @param {Object} event Submit event.
*/
function submitForm( event ) {
let hasError = false;
// Ensure `OrderItem`s.
if ( noItemErrorEl ) {
if ( 0 === orderItems.length ) {
noItemErrorEl.style.display = 'block';
hasError = true;
} else {
noItemErrorEl.style.display = 'none';
}
}
// Ensure Customer.
if ( noCustomerErrorEl ) {
if ( '0' === assignCustomerEl.value && '' === newCustomerEmailEl.value ) {
noCustomerErrorEl.style.display = 'block';
hasError = true;
} else {
noCustomerErrorEl.style.display = 'none';
}
if ( true === hasError ) {
event.preventDefault();
}
}
}
/**
* Remove `OrderItem` notice when an `OrderItem` is added.
*
* @since 3.0
*/
orderItems.on( 'add', function() {
noItemErrorEl.style.display = 'none';
} );
/**
* Remove Customer notice when a Customer is changed.
*
* Uses a jQuery binding for Chosen support.
*
* @since 3.0
*
* @param {Object} event Change event.
*/
$( assignCustomerEl ).on( 'change', ( event ) => {
const val = event.target.value;
if ( '0' !== val ) {
noCustomerErrorEl.style.display = 'none';
}
} )
if ( newCustomerEmailEl ) {
/**
* Remove Customer notice when a Customer is set.
*
* @since 3.0
*
* @param {Object} event Input event.
*/
newCustomerEmailEl.addEventListener( 'input', ( event ) => {
const val = event.target.value;
if ( '' !== val ) {
noCustomerErrorEl.style.display = 'none';
}
} );
}
} )();
}
// Move `.update-nag` items below the top header.
// `#update-nag` is legacy styling, which core still supports.
//
// `.notice` items are properly moved, but WordPress core
// does not move `.update-nag`.
if ( 0 !== $( '.edit-post-editor-regions__header' ).length ) {
$( 'div.update-nag, div#update-nag' ).insertAfter( $( '.edit-post-editor-regions__header' ) );
}
} );

View File

@ -0,0 +1,17 @@
/* global $, ajaxurl */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
$( '.download_page_edd-payment-history .row-actions .delete a' ).on( 'click', function() {
if( confirm( edd_vars.delete_payment ) ) {
return true;
}
return false;
});
} );

View File

@ -0,0 +1,256 @@
/* global $, ajaxurl, _ */
/**
* Internal dependencies
*/
import OrderOverview from './../order-overview';
import { getChosenVars } from 'utils/chosen.js';
import { jQueryReady } from 'utils/jquery.js';
// Store customer search results to help prefill address data.
let CUSTOMER_SEARCH_RESULTS = {
addresses: {
'0': {
address: '',
address2: '',
city: '',
region: '',
postal_code: '',
country: '',
},
},
};
jQueryReady( () => {
/**
* Adjusts Overview tax configuration when the Customer's address changes.
*
* @since 3.0
*/
( () => {
const { state: overviewState } = OrderOverview.options;
// No tax, do nothing.
if ( false === overviewState.get( 'hasTax' ) ) {
return;
}
// Editing, do nothing.
if ( false === overviewState.get( 'isAdding' ) ) {
return;
}
const countryInput = document.getElementById(
'edd_order_address_country'
);
const regionInput = document.getElementById(
'edd_order_address_region'
);
if ( ! ( countryInput && regionInput ) ) {
return;
}
/**
* Retrieves a tax rate based on the currently selected Address.
*
* @since 3.0
*/
function getTaxRate() {
const country = $( '#edd_order_address_country' ).val();
const region = $( '#edd_order_address_region' ).val();
const nonce = document.getElementById( 'edd_get_tax_rate_nonce' )
.value;
wp.ajax.send( 'edd_get_tax_rate', {
data: {
nonce,
country,
region,
},
/**
* Updates the Overview's tax configuration on successful retrieval.
*
* @since 3.0
*
* @param {Object} response AJAX response.
*/
success( response ) {
let { tax_rate: rate } = response;
// Make a percentage.
rate = rate * 100;
overviewState.set( 'hasTax', {
...overviewState.get( 'hasTax' ),
country,
region,
rate,
} );
},
/*
* Updates the Overview's tax configuration on failed retrieval.
*
* @since 3.0
*/
error() {
overviewState.set( 'hasTax', 'none' );
},
} );
}
// Update rate on Address change.
//
// Wait for Region field to be replaced when Country changes.
// Wait for typing when Regino field changes.
// jQuery listeners for Chosen compatibility.
$( '#edd_order_address_country' ).on( 'change', _.debounce( getTaxRate, 250 ) );
$( '#edd-order-address' ).on( 'change', '#edd_order_address_region', getTaxRate );
$( '#edd-order-address' ).on( 'keyup', '#edd_order_address_region', _.debounce( getTaxRate, 250 ) );
} )();
$( '.edd-payment-change-customer-input' ).on( 'change', function() {
const $this = $( this ),
data = {
action: 'edd_customer_addresses',
customer_id: $this.val(),
nonce: $( '#edd_add_order_nonce' ).val(),
};
$.post( ajaxurl, data, function( response ) {
const { success, data } = response;
if ( ! success ) {
$( '.customer-address-select-wrap' ).hide();
return;
}
// Store response for later use.
CUSTOMER_SEARCH_RESULTS = {
...CUSTOMER_SEARCH_RESULTS,
...data,
addresses: {
...CUSTOMER_SEARCH_RESULTS.addresses,
...data.addresses,
},
};
if ( data.html ) {
$( '.customer-address-select-wrap' ).show();
$( '.customer-address-select-wrap .edd-form-group__control' ).html( data.html );
} else {
$( '.customer-address-select-wrap' ).hide();
}
}, 'json' );
return false;
} );
/**
* Retrieves a list of states based on a Country HTML <select>.
*
* @since 3.0
*
* @param {HTMLElement} countryEl Element containing country information.
* @param {string} fieldName the name of the field to use in response.
* @return {$.promise} Region data response.
*/
function getStates( countryEl, fieldName, fieldId ) {
const data = {
action: 'edd_get_shop_states',
country: countryEl.val(),
nonce: countryEl.data( 'nonce' ),
field_name: fieldName,
field_id: fieldId,
};
return $.post( ajaxurl, data );
}
/**
* Replaces the Region area with the appropriate field type.
*
* @todo This is hacky and blindly picks elements from the DOM.
*
* @since 3.0
*
* @param {string} regions Regions response.
*/
function replaceRegionField( regions ) {
const state_wrapper = $( '#edd_order_address_region' );
$( '#edd_order_address_region_chosen' ).remove();
if ( 'nostates' === regions ) {
state_wrapper
.replaceWith( '<input type="text" name="edd_order_address[region]" id="edd_order_address_region" value="" class="wide-fat" style="max-width: none; width: 100%;" />' );
} else {
state_wrapper
.replaceWith( regions );
$( '#edd_order_address_region' ).chosen( getChosenVars( $( '#edd_order_address_region' ) ) );
}
}
/**
* Handles replacing a Region field when a Country field changes.
*
* @since 3.0
*/
function updateRegionFieldOnChange() {
getStates(
$( this ),
'edd_order_address[region]',
'edd_order_address_region'
)
.done( replaceRegionField );
}
$( document.body ).on( 'change', '.customer-address-select-wrap .add-order-customer-address-select', function() {
const $this = $( this ),
val = $this.val(),
address = CUSTOMER_SEARCH_RESULTS.addresses[ val ];
$( '#edd-add-order-form input[name="edd_order_address[address]"]' ).val( address.address );
$( '#edd-add-order-form input[name="edd_order_address[address2]"]' ).val( address.address2 );
$( '#edd-add-order-form input[name="edd_order_address[postal_code]"]' ).val( address.postal_code );
$( '#edd-add-order-form input[name="edd_order_address[city]"]' ).val( address.city );
$( '#edd-add-order-form input[name="edd_order_address[address_id]"]' ).val( val );
// Remove global `change` event handling to prevent loop.
$( '#edd_order_address_country' ).off( 'change', updateRegionFieldOnChange );
// Set Country.
$( '#edd_order_address_country' )
.val( address.country )
.trigger( 'change' )
.trigger( 'chosen:updated' );
// Set Region.
getStates(
$( '#edd_order_address_country' ),
'edd_order_address[region]',
'edd_order_address_region'
)
.done( replaceRegionField )
.done( ( response ) => {
$( '#edd_order_address_region' )
.val( address.region )
.trigger( 'change' )
.trigger( 'chosen:updated' );
} );
// Add back global `change` event handling.
$( '#edd_order_address_country' ).on( 'change', updateRegionFieldOnChange );
return false;
} );
// Country change.
$( '#edd_order_address_country' ).on( 'change', updateRegionFieldOnChange );
} );

View File

@ -0,0 +1,69 @@
/* global $ */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
// Change Customer.
$( '.edd-payment-change-customer-input' ).on( 'change', function() {
const $this = $( this ),
data = {
action: 'edd_customer_details',
customer_id: $this.val(),
nonce: $( '#edd_customer_details_nonce' ).val(),
};
if ( '' === data.customer_id ) {
return;
}
$( '.customer-details' ).css( 'display', 'none' );
$( '#customer-avatar' ).html( '<span class="spinner is-active"></span>' );
$.post( ajaxurl, data, function( response ) {
const { success, data } = response;
if ( success ) {
$( '.customer-details' ).css( 'display', 'flex' );
$( '.customer-details-wrap' ).css( 'display', 'flex' );
$( '#customer-avatar' ).html( data.avatar );
$( '.customer-name' ).html( data.name );
$( '.customer-since span' ).html( data.date_created_i18n );
$( '.customer-record a' ).prop( 'href', data._links.self );
} else {
$( '.customer-details-wrap' ).css( 'display', 'none' );
}
}, 'json' );
} );
$( '.edd-payment-change-customer-input' ).trigger( 'change' );
// New Customer.
$( '#edd-customer-details' ).on( 'click', '.edd-payment-new-customer, .edd-payment-new-customer-cancel', function( e ) {
e.preventDefault();
var new_customer = $( this ).hasClass( 'edd-payment-new-customer' ),
cancel = $( this ).hasClass( 'edd-payment-new-customer-cancel' );
if ( new_customer ) {
$( '.order-customer-info' ).hide();
$( '.new-customer' ).show();
} else if ( cancel ) {
$( '.order-customer-info' ).show();
$( '.new-customer' ).hide();
}
var new_customer = $( '#edd-new-customer' );
if ( $( '.new-customer' ).is( ':visible' ) ) {
new_customer.val( 1 );
} else {
new_customer.val( 0 );
}
} );
} );

View File

@ -0,0 +1,3 @@
import './address.js';
import './customer.js';
import './receipt.js';

View File

@ -0,0 +1,32 @@
/* global $, ajaxurl */
/**
* Internal dependencies
*/
import { jQueryReady } from 'utils/jquery.js';
jQueryReady( () => {
const emails_wrap = $( '.edd-order-resend-receipt-addresses' );
$( document.body ).on( 'click', '#edd-select-receipt-email', function( e ) {
e.preventDefault();
emails_wrap.slideDown();
} );
$( document.body ).on( 'change', '.edd-order-resend-receipt-email', function() {
const selected = $('input:radio.edd-order-resend-receipt-email:checked').val();
$( '#edd-select-receipt-email').data( 'email', selected );
} );
$( document.body).on( 'click', '#edd-select-receipt-email', function () {
if ( confirm( edd_vars.resend_receipt ) ) {
const href = $( this ).prop( 'href' ) + '&email=' + $( this ).data( 'email' );
window.location = href;
}
} );
$( document.body ).on( 'click', '#edd-resend-receipt', function() {
return confirm( edd_vars.resend_receipt );
} );
} );

View File

@ -0,0 +1,279 @@
import { NumberFormat } from '@easy-digital-downloads/currency';
const number = new NumberFormat();
/* global eddAdminOrderOverview */
// Loads the modal when the refund button is clicked.
$(document.body).on('click', '.edd-refund-order', function (e) {
e.preventDefault();
var link = $(this),
postData = {
action : 'edd_generate_refund_form',
order_id: $('input[name="edd_payment_id"]').val(),
};
$.ajax({
type : 'POST',
data : postData,
url : ajaxurl,
success: function success(data) {
let modal_content = '';
if (data.success) {
modal_content = data.html;
} else {
modal_content = data.message;
}
$('#edd-refund-order-dialog').dialog({
position: { my: 'top center', at: 'center center-25%' },
width : '75%',
modal : true,
resizable: false,
draggable: false,
classes: {
'ui-dialog': 'edd-dialog',
},
closeText: eddAdminOrderOverview.i18n.closeText,
open: function( event, ui ) {
$(this).html( modal_content );
},
close: function( event, ui ) {
$( this ).html( '' );
if ( $( this ).hasClass( 'did-refund' ) ) {
location.reload();
}
}
});
return false;
}
}).fail(function (data) {
$('#edd-refund-order-dialog').dialog({
position: { my: 'top center', at: 'center center-25%' },
width : '75%',
modal : true,
resizable: false,
draggable: false
}).html(data.message);
return false;
});
});
$( document.body ).on( 'click', '.ui-widget-overlay', function ( e ) {
$( '#edd-refund-order-dialog' ).dialog( 'close' );
} );
/**
* Listen for the bulk actions checkbox, since WP doesn't trigger a change on sub-items.
*/
$( document.body ).on( 'change', '#edd-refund-order-dialog #cb-select-all-1', function () {
const itemCheckboxes = $( '.edd-order-item-refund-checkbox' );
const isChecked = $( this ).prop( 'checked' );
itemCheckboxes.each( function() {
$( this ).prop( 'checked', isChecked ).trigger( 'change' );
} );
} );
/**
* Listen for individual checkbox changes.
* When it does, trigger a quantity change.
*/
$( document.body ).on( 'change', '.edd-order-item-refund-checkbox', function () {
const parent = $( this ).parent().parent();
const quantityField = parent.find( '.edd-order-item-refund-quantity' );
if ( quantityField.length ) {
if ( $( this ).prop( 'checked' ) ) {
// Triggering a change on the quantity field handles enabling the inputs.
quantityField.trigger( 'change' );
} else {
// Disable inputs and recalculate total.
parent.find( '.edd-order-item-refund-input' ).prop( 'disabled', true );
recalculateRefundTotal();
}
}
} );
/**
* Handles quantity changes, which includes items in the refund.
*/
$( document.body ).on( 'change', '#edd-refund-order-dialog .edd-order-item-refund-input', function () {
let parent = $( this ).closest( '.refunditem' ),
quantityField = parent.find( '.edd-order-item-refund-quantity' ),
quantity = parseInt( quantityField.val() );
if ( quantity > 0 ) {
parent.addClass( 'refunded' );
} else {
parent.removeClass( 'refunded' );
}
// Only auto calculate subtotal / tax if we've adjusted the quantity.
if ( $( this ).hasClass( 'edd-order-item-refund-quantity' ) ) {
// Enable/disable amount fields.
parent.find( '.edd-order-item-refund-input:not(.edd-order-item-refund-quantity)' ).prop( 'disabled', quantity === 0 );
if ( quantity > 0 ) {
quantityField.prop( 'disabled', false );
}
let subtotalField = parent.find( '.edd-order-item-refund-subtotal' ),
taxField = parent.find( '.edd-order-item-refund-tax' ),
originalSubtotal = number.unformat( subtotalField.data( 'original' ) ),
originalTax = taxField.length ? number.unformat( taxField.data( 'original' ) ) : 0.00,
originalQuantity = parseInt( quantityField.data( 'max' ) ),
calculatedSubtotal = ( originalSubtotal / originalQuantity ) * quantity,
calculatedTax = taxField.length ? ( originalTax / originalQuantity ) * quantity : 0.00;
// Make sure totals don't go over maximums.
if ( calculatedSubtotal > parseFloat( subtotalField.data( 'max' ) ) ) {
calculatedSubtotal = subtotalField.data( 'max' );
}
if ( taxField.length && calculatedTax > parseFloat( taxField.data( 'max' ) ) ) {
calculatedTax = taxField.data( 'max' );
}
// Guess the subtotal and tax for the selected quantity.
subtotalField.val( number.format( calculatedSubtotal ) );
if ( taxField.length ) {
taxField.val( number.format( calculatedTax ) );
}
}
recalculateRefundTotal();
} );
/**
* Calculates all the final refund values.
*/
function recalculateRefundTotal() {
let newSubtotal = 0,
newTax = 0,
newTotal = 0,
canRefund = false,
allInputBoxes = $( '#edd-refund-order-dialog .edd-order-item-refund-input' ),
allReadOnly = $( '#edd-refund-order-dialog .edd-order-item-refund-input.readonly' );
// Set a readonly while we recalculate, to avoid race conditions in the browser.
allInputBoxes.prop( 'readonly', true );
// Loop over all order items.
$( '#edd-refund-order-dialog .edd-order-item-refund-quantity' ).each( function() {
const thisItemQuantity = parseInt( $( this ).val() );
if ( ! thisItemQuantity ) {
return;
}
const thisItemParent = $( this ).closest( '.refunditem' );
const thisItemSelected = thisItemParent.find( '.edd-order-item-refund-checkbox' ).prop( 'checked' );
if ( ! thisItemSelected ) {
thisItemParent.removeClass( 'refunded' );
return;
}
// Values for this item.
let thisItemTax = 0.00;
let thisItemSubtotal = number.unformat( thisItemParent.find( '.edd-order-item-refund-subtotal' ).val() );
if ( thisItemParent.find( '.edd-order-item-refund-tax' ).length ) {
thisItemTax = number.unformat( thisItemParent.find( '.edd-order-item-refund-tax' ).val() );
}
let thisItemTotal = thisItemSubtotal + thisItemTax;
thisItemParent.find( '.column-total span' ).text( number.format( thisItemTotal ) );
// Negate amounts if working with credit.
if ( thisItemParent.data( 'credit' ) ) {
thisItemSubtotal = thisItemSubtotal * -1;
thisItemTax = thisItemTax * -1;
thisItemTotal = thisItemTotal * -1;
}
// Only include order items in the subtotal.
if ( thisItemParent.data( 'orderItem' ) ) {
newSubtotal += thisItemSubtotal;
}
newTax += thisItemTax;
newTotal += thisItemTotal;
} );
if ( parseFloat( newTotal ) > 0 ) {
canRefund = true;
}
$( '#edd-refund-submit-subtotal-amount' ).text( number.format( newSubtotal ) );
$( '#edd-refund-submit-tax-amount' ).text( number.format( newTax ) );
$( '#edd-refund-submit-total-amount' ).text( number.format( newTotal ) );
$( '#edd-submit-refund-submit' ).attr( 'disabled', ! canRefund );
// Remove the readonly.
allInputBoxes.prop( 'readonly', false );
allReadOnly.prop( 'readonly', true );
}
/**
* Process the refund form after the button is clicked.
*/
$(document.body).on( 'click', '#edd-submit-refund-submit', function(e) {
e.preventDefault();
$('.edd-submit-refund-message').removeClass('success').removeClass('fail');
$( this ).removeClass( 'button-primary' ).attr( 'disabled', true ).addClass( 'updating-message' );
$('#edd-submit-refund-status').hide();
const refundForm = $( '#edd-submit-refund-form' );
const refundData = refundForm.serialize();
var postData = {
action: 'edd_process_refund_form',
data: refundData,
order_id: $('input[name="edd_payment_id"]').val()
};
$.ajax({
type : 'POST',
data : postData,
url : ajaxurl,
success: function success(response) {
const message_target = $('.edd-submit-refund-message'),
url_target = $('.edd-submit-refund-url');
if ( response.success ) {
message_target.text(response.data.message).addClass('success');
url_target.attr( 'href', response.data.refund_url ).show();
$( '#edd-submit-refund-status' ).show();
url_target.focus();
$( '#edd-refund-order-dialog' ).addClass( 'did-refund' );
} else {
message_target.html(response.data).addClass('fail');
url_target.hide();
$('#edd-submit-refund-status').show();
$( '#edd-submit-refund-submit' ).attr( 'disabled', false ).removeClass( 'updating-message' ).addClass( 'button-primary' );
}
}
} ).fail( function ( data ) {
const message_target = $('.edd-submit-refund-message'),
url_target = $('.edd-submit-refund-url'),
json = data.responseJSON;
message_target.text( json.data ).addClass( 'fail' );
url_target.hide();
$( '#edd-submit-refund-status' ).show();
$( '#edd-submit-refund-submit' ).attr( 'disabled', false ).removeClass( 'updating-message' ).addClass( 'button-primary' );
return false;
});
});
// Initialize WP toggle behavior for the modal.
$( document.body ).on( 'click', '.refund-items .toggle-row', function () {
$( this ).closest( 'tr' ).toggleClass( 'is-expanded' );
} );

View File

@ -0,0 +1,101 @@
/* global Backbone */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './../models/order-adjustment.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
/**
* Collection of `OrderAdjustment`s.
*
* @since 3.0
*
* @class Adjustments
* @augments Backbone.Collection
*/
export const OrderAdjustments = Backbone.Collection.extend( {
/**
* @since 3.0
*/
comparator: 'type',
/**
* Initializes the `OrderAdjustments` collection.
*
* @since 3.0
*
* @constructs OrderAdjustments
* @augments Backbone.Collection
*/
initialize() {
this.getByType = this.getByType.bind( this );
},
/**
* Determines which Model to use and instantiates it.
*
* @since 3.0
*
* @param {Object} attributes Model attributes.
* @param {Object} options Model options.
*/
model( attributes, options ) {
let model;
switch ( attributes.type ) {
case 'discount':
model = new OrderAdjustmentDiscount( attributes, options );
break;
default:
model = new OrderAdjustment( attributes, options );
}
return model;
},
/**
* Defines the model's attribute that defines it's ID.
*
* Uses the `OrderAdjustment`'s Type ID.
*
* @since 3.0
*
* @param {Object} attributes Model attributes.
* @return {number}
*/
modelId( attributes ) {
return `${ attributes.type }-${ attributes.typeId }-${ attributes.description }`;
},
/**
* Determines if `OrderAdjustments` contains a specific `OrderAdjustment`.
*
* @since 3.0
*
* @param {OrderAdjustment} model Model to look for.
* @return {bool} True if the Collection contains the Model.
*/
has( model ) {
return (
undefined !==
this.findWhere( {
typeId: model.get( 'typeId' ),
} )
);
},
/**
* Returns a list of `OrderAdjustment`s by type.
*
* @since 3.0
*
* @param {string} type Type of adjustment to retrieve. `fee`, `credit`, or `discount`.
* @return {Array} List of type-specific adjustments.
*/
getByType( type ) {
return this.where( {
type,
} );
},
} );

View File

@ -0,0 +1,137 @@
/* global Backbone, $, _ */
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { OrderAdjustments } from './../collections/order-adjustments.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
import { OrderItem } from './../models/order-item.js';
/**
* Collection of `OrderItem`s.
*
* @since 3.0
*
* @class OrderItems
* @augments Backbone.Collection
*/
export const OrderItems = Backbone.Collection.extend( {
/**
* @since 3.0
*
* @type {OrderItem}
*/
model: OrderItem,
/**
* Ensures `OrderItems` has access to the current state through a similar
* interface as Views. BackBone.Collection does not automatically set
* passed options as a property.
*
* @since 3.0
*
* @param {null|Array} models List of Models.
* @param {Object} options Collection options.
*/
preinitialize( models, options ) {
this.options = options;
},
/**
* Determines if `OrderItems` contains a specific `OrderItem`.
*
* Uses the `OrderItem`s Product ID and Price ID to create a unique
* value to check against.
*
* @since 3.0
*
* @param {OrderItem} model Model to look for.
* @return {bool} True if the Collection contains the Model.
*/
has( model ) {
const duplicates = this.filter( ( item ) => {
const itemId =
item.get( 'productId' ) + '_' + item.get( 'priceId' );
const modelId =
model.get( 'productId' ) + '_' + model.get( 'priceId' );
return itemId === modelId;
} );
return duplicates.length > 0;
},
/**
* Updates the amounts for all current `OrderItem`s.
*
* @since 3.0
*
* @return {$.promise} A jQuery promise representing zero or more requests.
*/
updateAmounts() {
const { options } = this;
const { state } = options;
const items = state.get( 'items' );
const discounts = new Backbone.Collection(
state.get( 'adjustments' ).getByType( 'discount' )
);
const args = {
country: state.getTaxCountry(),
region: state.getTaxRegion(),
products: items.map( ( item ) => ( {
id: item.get( 'productId' ),
quantity: item.get( 'quantity' ),
options: {
price_id: item.get( 'priceId' ),
}
} ) ),
discountIds: discounts.pluck( 'typeId' ),
};
// Keep track of all jQuery Promises.
const promises = [];
// Find each `OrderItem`'s amounts.
items.models.forEach( ( item ) => {
const getItemAmounts = item.getAmounts( args );
getItemAmounts
// Update `OrderItem`-level Adjustments.
.done( ( { adjustments } ) => {
// Map returned Discounts to `OrderAdjustmentDiscount`.
const orderItemDiscounts = adjustments.map( ( adjustment ) => {
return new OrderAdjustmentDiscount( {
...adjustment,
id: uuid(),
objectId: item.get( 'id' ),
} );
} );
// Gather existing `fee` and `credit` `OrderItem`-level Adjustments.
const orderItemAdjustments = item.get( 'adjustments' ).filter( ( adjustment ) => {
return [ 'fee', 'credit' ].includes( adjustment.type );
} );
// Reset `OrderAdjustments` collection with new data.
item.set( 'adjustments', new OrderAdjustments( [
...orderItemDiscounts,
...orderItemAdjustments,
] ) );
} )
// Update individual `OrderItem`s and `OrderAdjustment`s with new amounts.
.done( ( response ) => item.setAmounts( response ) );
// Track jQuery Promise.
promises.push( getItemAmounts );
} );
return $.when.apply( $, promises );
},
} );

View File

@ -0,0 +1,21 @@
/* global Backbone */
/**
* Internal dependencies
*/
import { OrderRefund } from './../models/order-refund.js';
/**
* Collection of `OrderRefund`s.
*
* @since 3.0
*
* @class OrderRefunds
* @augments Backbone.Collection
*/
export const OrderRefunds = Backbone.Collection.extend( {
/**
* @since 3.0
*/
model: OrderRefund,
} );

View File

@ -0,0 +1,106 @@
/**
* Internal dependencies
*/
import { Currency, NumberFormat } from '@easy-digital-downloads/currency';
import { Overview } from './views/overview.js';
import { OrderItems } from './collections/order-items.js';
import { OrderItem } from './models/order-item.js';
import { OrderAdjustments } from './collections/order-adjustments.js';
import { OrderRefunds } from './collections/order-refunds.js';
import { State } from './models/state.js';
// Temporarily include old Refund flow.
import './_refund.js';
let overview;
( () => {
if ( ! window.eddAdminOrderOverview ) {
return;
}
const {
isAdding,
isRefund,
hasTax,
hasQuantity,
hasDiscounts,
order,
items,
adjustments,
refunds,
} = window.eddAdminOrderOverview;
const currencyFormatter = new Currency( {
currency: order.currency,
currencySymbol: order.currencySymbol,
} );
// Create and hydrate state.
const state = new State( {
isAdding: '1' === isAdding,
isRefund: '1' === isRefund,
hasTax: '0' === hasTax ? false : hasTax,
hasQuantity: '1' === hasQuantity,
hasDiscounts: '1' === hasDiscounts,
formatters: {
currency: currencyFormatter,
// Backbone doesn't merge nested defaults.
number: new NumberFormat(),
},
order,
} );
// Create collections and add to state.
state.set( {
items: new OrderItems( null, {
state,
} ),
adjustments: new OrderAdjustments( null, {
state,
} ),
refunds: new OrderRefunds( null, {
state,
} ),
} );
// Create Overview.
overview = new Overview( {
state,
} );
// Hydrate collections.
// Hydrate `OrderItem`s.
//
// Models are created manually before being added to the collection to
// ensure attributes maintain schema with deep model attributes.
items.forEach( ( item ) => {
const orderItemAdjustments = new OrderAdjustments( item.adjustments );
const orderItem = new OrderItem( {
...item,
adjustments: orderItemAdjustments,
state,
} );
state.get( 'items' ).add( orderItem );
} );
// Hyrdate `Order`-level `Adjustments`.
adjustments.forEach( ( adjustment ) => {
state.get( 'adjustments' ).add( {
state,
...adjustment,
} )
} );
// Hydrate `OrderRefund`s.
refunds.forEach( ( refund ) => {
state.get( 'refunds' ).add( {
state,
...refund,
} );
} );
} ) ();
export default overview;

View File

@ -0,0 +1,76 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './order-adjustment.js';
/**
* OrderAdjustmentDiscount
*
* @since 3.0
*
* @class OrderAdjustmentDiscount
* @augments Backbone.Model
*/
export const OrderAdjustmentDiscount = OrderAdjustment.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustmentDiscount
*/
defaults: {
...OrderAdjustment.prototype.defaults,
type: 'discount',
},
/**
* @since 3.0
*/
idAttribute: 'typeId',
/**
* Returns the `OrderAdjustmentDiscount`'s amount based on the current values
* of all `OrderItems` discounts.
*
* @since 3.0
*
* @return {number} `OrderAdjustmentDiscount` amount.
*/
getAmount() {
let amount = 0;
const state = this.get( 'state' );
// Return stored amount if viewing an existing Order.
if ( false === state.get( 'isAdding' ) ) {
return OrderAdjustment.prototype.getAmount.apply( this, arguments );
}
const { models: items } = state.get( 'items' );
const { number } = state.get( 'formatters' );
items.forEach( ( item ) => {
const discount = item.get( 'adjustments' ).findWhere( {
typeId: this.get( 'typeId' ),
} );
if ( undefined !== discount ) {
amount += number.unformat(
number.format( discount.get( 'subtotal' ) )
);
}
} );
return amount;
},
/**
* Returns the `OrderAdjustment` total.
*
* @since 3.0
*/
getTotal() {
return this.getAmount();
},
} );

View File

@ -0,0 +1,101 @@
/* global Backbone */
/**
* OrderAdjustment
*
* @since 3.0
*
* @class OrderAdjustment
* @augments Backbone.Model
*/
export const OrderAdjustment = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustment
*/
defaults: {
id: 0,
objectId: 0,
objectType: '',
typeId: 0,
type: '',
description: '',
subtotal: 0,
tax: 0,
total: 0,
dateCreated: '',
dateModified: '',
uuid: '',
},
/**
* Returns the `OrderAdjustment` amount.
*
* Separate from subtotal or total calculation so `OrderAdjustmentDiscount`
* can be calculated independently.
*
* @see OrderAdjustmentDiscount.prototype.getAmount()
*
* @since 3.0
*/
getAmount() {
return this.get( 'subtotal' );
},
/**
* Retrieves the `OrderAdjustment` tax.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTax() {
return this.get( 'tax' );
},
/**
* Returns the `OrderAdjustment` total.
*
* @since 3.0
*/
getTotal() {
// Fees always have tax added exclusively.
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/issues/2445#issuecomment-53215087
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/f97f4f6f5454921a2014dc1fa8f4caa5f550108c/includes/cart/class-edd-cart.php#L1306-L1311
return this.get( 'subtotal' ) + this.get( 'tax' );
},
/**
* Recalculates the tax amount based on the current tax rate.
*
* @since 3.0.0
*/
updateTax() {
const state = this.get( 'state' );
const hasTax = state.get( 'hasTax' );
if (
'none' === hasTax ||
'' === hasTax.country ||
'' === hasTax.rate
) {
return;
}
const { number } = state.get( 'formatters' );
const taxRate = hasTax.rate / 100;
const adjustments = state.get( 'adjustments' ).getByType( 'fee' );
adjustments.forEach( ( adjustment ) => {
if ( false === adjustment.get( 'isTaxable' ) ) {
return;
}
const taxableAmount = adjustment.getAmount();
const taxAmount = number.unformat( taxableAmount * taxRate );
adjustment.set( 'tax', taxAmount );
} );
}
} );

View File

@ -0,0 +1,250 @@
/* global Backbone, _, $ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './../collections/order-adjustments.js';
/**
* OrderItem
*
* @since 3.0
*
* @class OrderItem
* @augments Backbone.Model
*/
export const OrderItem = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderItem
*/
defaults: {
id: 0,
orderId: 0,
productId: 0,
productName: '',
priceId: null,
cartIndex: 0,
type: 'download',
status: '',
statusLabel: '',
quantity: 1,
amount: 0,
subtotal: 0,
discount: 0,
tax: 0,
total: 0,
dateCreated: '',
dateModified: '',
uuid: '',
// Track manually set amounts.
amountManual: 0,
taxManual: 0,
subtotalManual: 0,
// Track if the amounts have been adjusted manually on addition.
_isAdjustingManually: false,
// Track `OrderItem`-level adjustments.
//
// The handling of Adjustments in the API is currently somewhat
// fragmented with certain extensions creating Adjustments at the
// `Order` level, some at a duplicate `OrderItem` level, and some both.
adjustments: new OrderAdjustments(),
},
/**
* Returns the `OrderItem` subtotal amount.
*
* @since 3.0.0
*
* @param {bool} includeTax If taxes should be included when retrieving the subtotal.
* This is needed in some scenarios with inclusive taxes.
* @return {number} Subtotal amount.
*/
getSubtotal( includeTax = false ) {
const state = this.get( 'state' );
const subtotal = this.get( 'subtotal' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return subtotal;
}
// Calculate subtotal.
if ( true === state.hasInclusiveTax() && false === includeTax ) {
return subtotal - this.getTax();
}
return subtotal;
},
/**
* Returns the Discount amount.
*
* If an Order is being added the amount is calculated based
* on the total of `OrderItem`-level Adjustments that are
* currently applied.
*
* If an Order has already been added use the amount stored
* directly in the database.
*
* @since 3.0
*
* @return {number} Discount amount.
*/
getDiscountAmount() {
let amount = 0;
const discounts = this.get( 'adjustments' ).getByType( 'discount' );
if ( 0 === discounts.length ) {
return this.get( 'discount' );
}
discounts.forEach( ( discount ) => {
amount += +discount.get( 'subtotal' );
} );
return amount;
},
/**
* Retrieves the rounded Tax for the order item.
*
* Rounded to match storefront checkout.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTax() {
const state = this.get( 'state' );
const tax = this.get( 'tax' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return tax;
}
// Calculate tax.
const { number } = state.get( 'formatters' );
return number.unformat( number.format( tax ) );
},
/**
* Retrieves the Total for the order item.
*
* @since 3.0.0
*
* @return {number} Total amount.
*/
getTotal() {
const state = this.get( 'state' );
// Use stored value if the record has already been created.
if ( false === state.get( 'isAdding' ) ) {
return this.get( 'total' );
}
// Calculate total.
if ( true === state.hasInclusiveTax() ) {
return this.get( 'subtotal' ) - this.getDiscountAmount();
}
return ( this.get( 'subtotal' ) - this.getDiscountAmount() ) + this.getTax();
},
/**
* Retrieves amounts for the `OrderItem` based on other `OrderItem`s and `OrderAdjustment`s.
*
* @since 3.0
*
* @param {Object} args Arguments to pass as data in the XHR request.
* @param {string} args.country Country code to determine tax rate.
* @param {string} args.region Region to determine tax rate.
* @param {Array} args.products List of current products added to the order.
* @param {Array} args.discountIds List of `OrderAdjustmentDiscount`s to calculate amounts against.
* @return {$.promise} A jQuery promise that represents the request.
*/
getAmounts( {
country = '',
region = '',
products = [],
discountIds = [],
} ) {
const {
nonces: { edd_admin_order_get_item_amounts: nonce },
} = window.eddAdminOrderOverview;
const { productId, priceId, quantity, amount, tax, subtotal } = _.clone(
this.attributes
);
return wp.ajax.send( 'edd-admin-order-get-item-amounts', {
data: {
nonce,
productId,
priceId,
quantity,
amount,
tax,
subtotal,
country,
region,
products: _.uniq( [
...products,
{
id: productId,
quantity,
options: {
price_id: priceId,
},
},
], function( { id, options: { price_id } } ) {
return `${ id }_${ price_id }`
} ),
discounts: _.uniq( discountIds ),
},
} );
},
/**
* Bulk sets amounts.
*
* Only adjusts the Discount amount if adjusting manually.
*
* @since 3.0
*
* @param {Object} amounts Amounts to set.
* @param {number} amounts.amount `OrderItem` unit price.
* @param {number} amounts.discount `OrderItem` discount amount.
* @param {number} amounts.tax `OrderItem` tax amount.
* @param {number} amounts.subtotal `OrderItem` subtotal amount.
* @param {number} amounts.total `OrderItem` total amount.
*/
setAmounts( {
amount = 0,
discount = 0,
tax = 0,
subtotal = 0,
total = 0,
} ) {
if ( true === this.get( '_isAdjustingManually' ) ) {
this.set( {
discount,
} );
} else {
this.set( {
amount,
discount,
tax,
subtotal,
total,
} );
}
},
} );

View File

@ -0,0 +1,24 @@
/* global Backbone */
/**
* OrderRefund
*
* @since 3.0
*
* @class OrderRefund
* @augments Backbone.Model
*/
export const OrderRefund = Backbone.Model.extend( {
/**
* @since 3.0
*
* @typedef {Object} OrderAdjustment
*/
defaults: {
id: 0,
number: '',
total: 0,
dateCreated: '',
dateCreatedi18n: '',
},
} );

View File

@ -0,0 +1,233 @@
/* global Backbone, _ */
/**
* Internal dependencies
*/
import { Currency, NumberFormat } from '@easy-digital-downloads/currency';
/**
* State
*
* Leverages `Backbone.Model` and subsequently `Backbone.Events`
* to easily track changes to top level state changes.
*
* @since 3.0
*
* @class State
* @augments Backbone.Model
*/
export const State = Backbone.Model.extend(
/** Lends State.prototype */ {
/**
* @since 3.0
*
* @typedef {Object} State
*/
defaults: {
isAdding: false,
isFetching: false,
hasQuantity: false,
hasTax: false,
items: [],
adjustments: [],
refunds: [],
formatters: {
currency: new Currency(),
number: new NumberFormat(),
},
},
/**
* Returns the current tax rate's country code.
*
* @since 3.0
*
* @return {string} Tax rate country code.
*/
getTaxCountry() {
return false !== this.get( 'hasTax' )
? this.get( 'hasTax' ).country
: '';
},
/**
* Returns the current tax rate's region.
*
* @since 3.0
*
* @return {string} Tax rate region.
*/
getTaxRegion() {
return false !== this.get( 'hasTax' )
? this.get( 'hasTax' ).region
: '';
},
/**
* Retrieves the Order subtotal.
*
* @since 3.0
*
* @param {bool} includeTax If taxes should be included when retrieving the subtotal.
* This is needed in some scenarios with inclusive taxes.
* @return {number} Order subtotal.
*/
getSubtotal( includeTax = false ) {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).subtotal;
}
const { models: items } = this.get( 'items' );
return items.reduce(
( amount, item ) => {
return amount += +item.getSubtotal( includeTax );
},
0
);
},
/**
* Retrieves the Order discount.
*
* @since 3.0
*
* @return {number} Order discount.
*/
getDiscount() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).discount;
}
const adjustments = this.get( 'adjustments' ).getByType( 'discount' );
return adjustments.reduce(
( amount, adjustment ) => {
return amount += +adjustment.getAmount();
},
0
);
},
/**
* Retrieves the Order tax.
*
* @since 3.0
*
* @return {number} Order tax.
*/
getTax() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).tax;
}
const items = this.get( 'items' ).models;
const feesTax = this.getFeesTax();
return items.reduce(
( amount, item ) => {
return amount += +item.getTax();
},
feesTax
);
},
/**
* Retrieves the Order tax amount for fees.
*
* @since 3.0
*
* @return {number} Order tax amount for fees.
*/
getFeesTax() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).tax;
}
const adjustments = this.get( 'adjustments' ).getByType( 'fee' );
return adjustments.reduce(
( amount, item ) => {
return amount += +item.getTax();
},
0
);
},
/**
* Retrieves the Order total.
*
* @since 3.0
*
* @return {number} Order total.
*/
getTotal() {
// Use stored value if the record has already been created.
if ( false === this.get( 'isAdding' ) ) {
return this.get( 'order' ).total;
}
// Calculate all adjustments that affect the total.
const { models: adjustments } = this.get( 'adjustments' );
const includeTaxInSubtotal = true;
const adjustedSubtotal = adjustments.reduce(
( amount, adjustment ) => {
if (
[ 'discount', 'credit' ].includes(
adjustment.get( 'type' )
)
) {
return amount -= +adjustment.getAmount();
} else {
return amount += +adjustment.get( 'subtotal' );
}
},
this.getSubtotal( includeTaxInSubtotal )
);
if ( true === this.hasInclusiveTax() ) {
// Fees always have tax added exclusively.
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/issues/2445#issuecomment-53215087
// @link https://github.com/easydigitaldownloads/easy-digital-downloads/blob/f97f4f6f5454921a2014dc1fa8f4caa5f550108c/includes/cart/class-edd-cart.php#L1306-L1311
return adjustedSubtotal + this.getFeesTax();
}
return adjustedSubtotal + this.getTax();
},
/**
* Determines if the state has a new, valid, tax rate.
*
* @since 3.0
*
* @return {bool} True if the rate has changed.
*/
hasNewTaxRate() {
const hasTax = this.get( 'hasTax' );
if ( false === hasTax ) {
return false;
}
const prevHasTax = this.previous( 'hasTax' );
return ! _.isEqual( hasTax, prevHasTax );
},
/**
* Determines if the state has prices entered inclusive of tax.
*
* @since 3.0
*
* @returns {bool} True if prices are entered inclusive of tax.
*/
hasInclusiveTax() {
return this.get( 'hasTax' ).inclusive;
}
}
);

View File

@ -0,0 +1,91 @@
/**
* Internal dependencies
*/
import { edd_attach_tooltips as setupTooltips } from 'admin/components/tooltips';
import { FormAddOrderItem } from './form-add-order-item.js';
import { FormAddOrderDiscount } from './form-add-order-discount.js';
import { FormAddOrderAdjustment } from './form-add-order-adjustment.js';
/**
* Actions
*
* @since 3.0
*
* @class Actions
* @augments wp.Backbone.View
*/
export const Actions = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview-actions',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-actions' ),
/**
* @since 3.0
*/
events: {
'click #add-item': 'onAddOrderItem',
'click #add-discount': 'onAddOrderDiscount',
'click #add-adjustment': 'onAddOrderAdjustment',
},
/**
* Ensures tooltips can be used after render.
*
* @since 3.0
*
* @return {Object}
*/
render() {
wp.Backbone.View.prototype.render.apply( this, arguments );
// Setup Tooltips after render.
setupTooltips( $( '.edd-help-tip' ) );
return this;
},
/**
* Renders the "Add Item" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderItem( e ) {
e.preventDefault();
new FormAddOrderItem( this.options ).openDialog().render();
},
/**
* Renders the "Add Discount" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderDiscount( e ) {
e.preventDefault();
new FormAddOrderDiscount( this.options ).openDialog().render();
},
/**
* Renders the "Add Adjustment" flow.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onAddOrderAdjustment( e ) {
e.preventDefault();
new FormAddOrderAdjustment( this.options ).openDialog().render();
},
} );

View File

@ -0,0 +1,255 @@
/* global _, $ */
/**
* WordPress dependencies
*/
import { focus } from '@wordpress/dom';
/**
* Internal dependencies
*/
import { getChosenVars } from 'utils/chosen.js';
// Set noconflict when using Lodash (@wordpress packages) and Underscores.
// @todo Find a better place to set this up. Webpack?
window.lodash = _.noConflict();
/**
* Base
*
* Supplies additional functionality and helpers beyond
* what is provided by `wp.Backbone.View`.
*
* - Maintains focus and caret positioning on rendering.
* - Extends events via `addEvents()`.
*
* @since 3.0
*
* @class Base
* @augments wp.Backbone.View
*/
export const Base = wp.Backbone.View.extend( {
/**
* Defines base events to help maintain focus and caret position.
*
* @since 3.0
*/
events: {
'keydown input': 'handleTabBehavior',
'keydown textarea': 'handleTabBehavior',
'focus input': 'onFocus',
'focus textarea': 'onFocus',
'focus select': 'onFocus',
'change input': 'onChange',
'change textarea': 'onChange',
'change select': 'onChange',
},
/**
* Sets up additional properties.
*
* @since 3.0
*/
preinitialize() {
this.focusedEl = null;
this.focusedElCaretPos = 0;
wp.Backbone.View.prototype.preinitialize.apply( this, arguments );
},
/**
* Merges additional events with existing events.
*
* @since 3.0
*
* @param {Object} events Hash of events to add.
*/
addEvents( events ) {
this.delegateEvents( {
...this.events,
...events,
} );
},
/**
* Moves the focus when dealing with tabbing.
*
* @since 3.0
*
* @param {Object} e Keydown event.
*/
handleTabBehavior( e ) {
const { keyCode, shiftKey, target } = e;
// 9 = TAB
if ( 9 !== keyCode ) {
return;
}
const tabbables = focus.tabbable.find( this.el );
if ( ! tabbables.length ) {
return;
}
const firstTabbable = tabbables[ 0 ];
const lastTabbable = tabbables[ tabbables.length - 1 ];
let toFocus;
if ( shiftKey && target === firstTabbable ) {
toFocus = lastTabbable;
} else if ( ! shiftKey && target === lastTabbable ) {
toFocus = firstTabbable;
} else if ( shiftKey ) {
toFocus = focus.tabbable.findPrevious( target );
} else {
toFocus = focus.tabbable.findNext( target );
}
if ( 'undefined' !== typeof toFocus ) {
this.focusedEl = toFocus;
this.focusedElCartetPos = toFocus.value.length;
} else {
this.focusedEl = null;
this.focusedElCartetPos = 0;
}
},
/**
* Tracks the current element when focusing.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onFocus( e ) {
this.focusedEl = e.target;
},
/**
* Tracks the current cursor position when editing.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChange( e ) {
const { target, keyCode } = e;
// 9 = TAB
if ( undefined !== typeof keyCode && 9 === keyCode ) {
return;
}
try {
if ( target.selectionStart ) {
this.focusedElCaretPos = target.selectionStart;
}
} catch ( error ) {
this.focusedElCaretPos = target.value.length;
}
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
return this.model
? {
...this.model.toJSON(),
state: this.model.get( 'state' ).toJSON(),
}
: {};
},
/**
* Adds additional handling after initial render.
*
* @since 3.0
*/
render() {
wp.Backbone.View.prototype.render.apply( this, arguments );
this.initializeSelects();
this.setFocus();
return this;
},
/**
* Reinitializes special <select> fields.
*
* @since 3.0
*/
initializeSelects() {
const selects = this.el.querySelectorAll( '.edd-select-chosen' );
// Reinialize Chosen.js
_.each( selects, ( el ) => {
$( el ).chosen( {
...getChosenVars( $( el ) ),
width: '100%',
} );
} );
},
/**
* Sets the focus and caret position.
*
* @since 3.0
*/
setFocus() {
const { el, focusedEl, focusedElCaretPos } = this;
// Do nothing extra if nothing is focused.
if ( null === focusedEl || 'undefined' === typeof focusedEl ) {
return;
}
// Convert full element in to a usable selector.
// We can't search for the actual HTMLElement because
// the DOM has since changed.
let selector = null;
if ( '' !== focusedEl.id ) {
selector = `#${ focusedEl.id }`;
} else if ( '' !== focusedEl.name ) {
selector = `[name="${ focusedEl.name }"]`;
} else if ( focusedEl.classList.length > 0 ) {
selector = `.${ [ ...focusedEl.classList ].join( '.' ) }`;
}
// Do nothing if we can't generate a selector.
if ( null === selector ) {
return;
}
// Focus element.
const elToFocus = el.querySelector( selector );
if ( ! elToFocus ) {
return;
}
elToFocus.focus();
// Attempt to set the caret position.
try {
if ( elToFocus.setSelectionRange ) {
elToFocus.setSelectionRange(
focusedElCaretPos,
focusedElCaretPos
);
}
} catch ( error ) {}
},
} );

View File

@ -0,0 +1,113 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
/**
* "Copy Download Link" view
*
* @since 3.0
*
* @class FormAddOrderItem
* @augments Dialog
*/
export const CopyDownloadLink = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-copy-download-link-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-copy-download-link' ),
/**
* "Copy Download Link" view.
*
* @since 3.0
*
* @constructs CopyDownloadLink
* @augments Base
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
this.link = false;
this.addEvents( {
'click #close': 'closeDialog',
} );
this.fetchLink.call( this );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { link } = this;
return {
link,
};
},
/**
* Renders the view.
*
* @since 3.0
*/
render() {
Base.prototype.render.apply( this, arguments );
const { el, link } = this;
// Select the contents if a link is available.
if ( false !== link && '' !== link ) {
el.querySelector( '#link' ).select();
}
},
/**
* Fetches the Download's file URLs.
*
* @since 3.0
*/
fetchLink() {
const { orderId, productId, priceId } = this.options;
// Retrieve and set link.
//
// We can't use wp.ajax.send because the `edd_ajax_generate_file_download_link()`
// does not send back JSON responses.
$.ajax( {
type: 'POST',
url: ajaxurl,
data: {
action: 'edd_get_file_download_link',
payment_id: orderId,
download_id: productId,
price_id: priceId,
},
} )
.done( ( link ) => {
link = link.trim();
if ( [ '-1', '-2', '-3', '-4', '' ].includes( link ) ) {
this.link = '';
} else {
this.link = link.trim();
}
} )
.done( () => this.render() );
},
} );

View File

@ -0,0 +1,79 @@
/* global eddAdminOrderOverview */
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* "Dialog" view
*
* @since 3.0
*
* @class Dialog
* @augments Base
*/
export const Dialog = Base.extend( {
/**
* "Dialog" view.
*
* @since 3.0
*
* @constructs Dialog
* @augments wp.Backbone.View
*/
initialize() {
this.$el.dialog( {
position: {
my: 'top center',
at: 'center center-25%',
},
classes: {
'ui-dialog': 'edd-dialog',
},
closeText: eddAdminOrderOverview.i18n.closeText,
width: '350px',
modal: true,
resizable: false,
draggable: false,
autoOpen: false,
create: function() {
$( this ).css( 'maxWidth', '90vw' );
},
} );
},
/**
* Opens the jQuery UI Dialog containing this view.
*
* @since 3.0
*
* @return {Dialog} Current view.
*/
openDialog() {
this.$el.dialog( 'open' );
return this;
},
/**
* Closes the jQuery UI Dialog containing this view.
*
* @since 3.0
*
* @param {Object=} e Event that triggered the close.
* @return {Dialog} Current view.
*/
closeDialog( e ) {
if ( e && e.preventDefault ) {
e.preventDefault();
}
this.$el.dialog( 'close' );
// Prevent events from stacking.
this.undelegateEvents();
return this;
},
} );

View File

@ -0,0 +1,289 @@
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
import { OrderAdjustment } from './../models/order-adjustment.js';
/**
* FormAddOrderAdjustment
*
* @since 3.0
*
* @class FormAddOrderAdjustment
* @augments Dialog
*/
export const FormAddOrderAdjustment = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-adjustment-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-adjustment' ),
/**
* "Add Adjustment" view.
*
* @since 3.0
*
* @constructs FormAddOrderAdjustment
* @augments Dialog
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
// Delegate additional events.
this.addEvents( {
'change #object_type': 'onChangeObjectType',
'change [name="type"]': 'onChangeType',
'keyup #amount': 'onChangeAmount',
'change #no-tax': 'onHasTaxToggle',
'click #set-address': 'onSetAddress',
'keyup #description': 'onChangeDescription',
'submit form': 'onAdd',
} );
const { state } = this.options;
// Create a model `OrderAdjustment` to be added.
this.model = new OrderAdjustment( {
id: uuid(),
objectId: uuid(),
typeId: uuid(),
objectType: 'order',
type: 'fee',
amountManual: '',
isTaxed: true,
state,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
return {
...Base.prototype.prepare.apply( this, arguments ),
// Pass existing OrderItems so we can apply a fee at OrderItem level.
orderItems: state.get( 'items' ).models.map( ( item ) => ( {
id: item.get( 'id' ),
productName: item.get( 'productName' ),
} ) ),
};
},
/**
* Updates the `OrderAdjustment` when the Object Type changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeObjectType( e ) {
const {
target: { options, selectedIndex },
} = e;
const selected = options[ selectedIndex ];
const objectType = selected.value;
let objectId = this.model.get( 'objectId' );
// Apply to a specific `OrderItem`.
if ( 'order_item' === objectType ) {
objectId = selected.dataset.orderItemId;
this.model.set( {
objectId,
objectType,
} );
// Apply to the whole order.
} else {
this.model.set( {
objectType,
objectId,
} );
}
},
/**
* Updates the `OrderAdjustment` when the Type changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeType( e ) {
const type = e.target.value;
this.model.set( 'type', type );
if ( 'credit' === type ) {
this.model.set( 'objectId', 0 );
this.model.set( 'objectType', 'order' );
}
},
/**
* Updates the `OrderAdjustment` when the Amount changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeAmount( e ) {
const { target } = e;
e.preventDefault();
const { state } = this.options;
const { number } = state.get( 'formatters' );
const amountManual = target.value;
const amountNumber = number.unformat( amountManual );
let taxNumber = 0;
const hasTax = state.get( 'hasTax' );
if (
true === this.model.get( 'isTaxed' ) &&
'fee' === this.model.get( 'type' ) &&
'none' !== hasTax &&
'' !== hasTax.country &&
'' !== hasTax.rate
) {
taxNumber = amountNumber * ( hasTax.rate / 100 );
}
this.model.set( {
amountManual,
subtotal: amountNumber,
total: amountNumber,
tax: number.unformat( number.format( taxNumber ) ),
} );
},
/**
* Toggles if the fee should be taxed.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onHasTaxToggle( e ) {
e.preventDefault();
const checked = e.target.checked;
const args = {
isTaxed: checked,
}
// Reset tax amount if it should not be taxed.
if ( false === checked ) {
args.tax = 0;
}
this.model.set( args );
},
/**
* Closes dialog and opens "Order Details - Address" section.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onSetAddress( e ) {
e.preventDefault();
this.closeDialog();
const button = $( '[href="#edd_general_address"]' );
if ( ! button ) {
return;
}
button.trigger( 'click' );
$( '#edd_order_address_country' ).trigger( 'focus' );
},
/**
* Updates the `OrderAdjustment` when the Description changes.
*
* @since 3.0
*
* @param {Object} e Change event
*/
onChangeDescription( e ) {
this.model.set( 'description', e.target.value );
},
/**
* Adds an `OrderAdjustment` to `OrderAdjustments`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
const adjustments = state.get( 'adjustments' );
const items = state.get( 'items' );
// Add at `OrderItem` level if necessary.
if ( 'order_item' === model.get( 'objectType' ) ) {
const orderItem = items.findWhere( {
id: model.get( 'objectId' ),
} );
orderItem.get( 'adjustments' ).add( model );
// Adding to the Collection doesn't bubble up a change event.
orderItem.trigger( 'change' );
model.set( 'objectType', 'order_item' );
} else {
// Add to `Order` level.
model.set( 'objectType', 'order' );
}
adjustments.add( model );
// Stop listening to the model in this view.
this.stopListening( model );
},
} );

View File

@ -0,0 +1,155 @@
/* global _ */
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Dialog } from './dialog.js';
import { Base } from './base.js';
import { OrderAdjustmentDiscount } from './../models/order-adjustment-discount.js';
/**
* "Add Discount" view
*
* @since 3.0
*
* @class FormAddOrderDiscount
* @augments wp.Backbone.View
*/
export const FormAddOrderDiscount = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-discount-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-discount' ),
/**
* @since 3.0
*/
events: {
'submit form': 'onAdd',
'change #discount': 'onChangeDiscount',
},
/**
* "Add Discount" view.
*
* @since 3.0
*
* @constructs FormAddOrderAdjustment
* @augments wp.Backbone.View
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
const { state } = this.options;
// Create a fresh `OrderAdjustmentDiscount` to be added.
this.model = new OrderAdjustmentDiscount( {
id: uuid(),
typeId: uuid(),
state,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const _isDuplicate = state.get( 'adjustments' ).has( model );
return {
...Base.prototype.prepare.apply( this, arguments ),
_isDuplicate,
};
},
/**
* Updates the `OrderDiscounts` when the Discount changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeDiscount( e ) {
const { target: { selectedIndex, options } } = e;
const { model } = this;
e.preventDefault();
const discount = options[ selectedIndex ];
const adjustment = discount.dataset;
if ( '' === discount.value ) {
return model.set( OrderAdjustmentDiscount.prototype.defaults );
}
model.set( {
typeId: parseInt( discount.value ),
description: adjustment.code,
} );
},
/**
* Adds an `OrderAdjustmentDiscount` to `OrderAdjustments`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
state.set( 'isFetching', true );
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Add to collection but do not alert.
adjustments.add( model, {
silent: true,
} );
// Update all amounts with new item and alert when done.
items
.updateAmounts()
.done( () => {
// Stop listening to the model in this view.
this.stopListening( model );
// Alert of succesful addition.
adjustments.trigger( 'add', model );
// Clear fetching.
state.set( 'isFetching', false ) ;
} );
},
} );

View File

@ -0,0 +1,364 @@
/**
* External dependencies
*/
import uuid from 'uuid-random';
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { Dialog } from './dialog.js';
import { OrderItem } from './../models/order-item.js';
/**
* "Add Item" view
*
* @since 3.0
*
* @class FormAddOrderItem
* @augments Dialog
*/
export const FormAddOrderItem = Dialog.extend( {
/**
* @since 3.0
*/
el: '#edd-admin-order-add-item-dialog',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-form-add-order-item' ),
/**
* "Add Item" view.
*
* @since 3.0
*
* @constructs FormAddOrderItem
* @augments Base
*/
initialize() {
Dialog.prototype.initialize.apply( this, arguments );
// Delegate additional events.
this.addEvents( {
'change #download': 'onChangeDownload',
'change #quantity': 'onChangeQuantity',
'change #auto-calculate': 'onAutoCalculateToggle',
'keyup #amount': 'onChangeAmount',
'keyup #tax': 'onChangeTax',
'keyup #subtotal': 'onChangeSubtotal',
'click #set-address': 'onSetAddress',
'submit form': 'onAdd',
} );
const { state } = this.options;
const id = uuid();
// Create a fresh `OrderItem` to be added.
this.model = new OrderItem( {
id,
orderId: id,
state,
error: false,
} );
// Listen for events.
this.listenTo( this.model, 'change', this.render );
this.listenTo( state, 'change:isFetching', this.render );
this.listenTo( state.get( 'items' ), 'add', this.closeDialog );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
const quantity = model.get( 'quantity' );
let amount = number.format( model.get( 'amount' ) * quantity );
let tax = number.format( model.get( 'tax' ) * quantity );
let subtotal = number.format( model.get( 'subtotal' ) * quantity );
if ( true === model.get( '_isAdjustingManually' ) ) {
amount = model.get( 'amountManual' );
tax = model.get( 'taxManual' );
subtotal = model.get( 'subtotalManual' );
}
const isDuplicate = false === state.get( 'isFetching' ) && true === state.get( 'items' ).has( model );
const isAdjustingManually = model.get( '_isAdjustingManually' );
const error = model.get( 'error' );
const defaults = Base.prototype.prepare.apply( this, arguments );
return {
...defaults,
amountManual: amount,
taxManual: tax,
subtotalManual: subtotal,
state: {
...defaults.state,
isAdjustingManually,
isDuplicate,
error,
}
};
},
/**
* Updates the OrderItem when the Download changes.
*
* @since 3.0
*
* @param {Object} e Change event for Download selector.
*/
onChangeDownload( e ) {
const {
target: { options: selectOptions, selectedIndex },
} = e;
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
// Find the selected Download.
const selected = selectOptions[ selectedIndex ];
// Set ID and Price ID.
let productId = selected.value;
let priceId = null;
const parts = productId.split( '_' );
productId = parseInt( parts[ 0 ] );
if ( parts[ 1 ] ) {
priceId = parseInt( parts[ 1 ] );
}
state.set( 'isFetching', true );
// Update basic attributes.
model.set( {
productId,
priceId,
productName: selected.text,
error: false,
} );
// Update amount attributes.
model
.getAmounts( {
country: state.getTaxCountry(),
region: state.getTaxRegion(),
products: state.get( 'items' ).map( ( item ) => ( {
id: item.get( 'productId' ),
quantity: item.get( 'quantity' ),
options: {
price_id: item.get( 'priceId' ),
}
} ) ),
discountIds: state.get( 'adjustments' ).pluck( 'typeId' ),
} )
.fail( ( { message: error } ) => {
// Clear fetching.
state.set( 'isFetching', false );
// Set error and reset model.
model.set( {
error,
productId: 0,
priceId: null,
productName: '',
} );
} )
.then( ( response ) => {
const { amount, tax, subtotal, total } = response;
model.set( {
amount,
tax,
subtotal,
total,
amountManual: number.format( amount ),
taxManual: number.format( tax ),
subtotalManual: number.format( subtotal ),
} );
// Clear fetching.
state.set( 'isFetching', false );
} );
},
/**
* Updates the `OrderItem`'s when the Quantity changes.
*
* @since 3.0
* @todo Validate.
*
* @param {Object} e Change event.
*/
onChangeQuantity( e ) {
this.model.set( 'quantity', parseInt( e.target.value ) );
},
/**
* Updates the `OrderItem`'s when the manually managed Amount changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeAmount( e ) {
this.model.set( 'amountManual', e.target.value );
},
/**
* Updates the `OrderItem`'s when the manually managed Tax changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeTax( e ) {
this.model.set( 'taxManual', e.target.value );
},
/**
* Updates the `OrderItem`'s when the manually managed Subtotal changes.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onChangeSubtotal( e ) {
this.model.set( 'subtotalManual', e.target.value );
},
/**
* Toggles manual amount adjustments.
*
* @since 3.0
*
* @param {Object} e Change event.
*/
onAutoCalculateToggle( e ) {
e.preventDefault();
this.model.set( {
_isAdjustingManually: ! e.target.checked,
} );
},
/**
* Closes dialog and opens "Order Details - Address" section.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onSetAddress( e ) {
e.preventDefault();
this.closeDialog();
const button = $( '[href="#edd_general_address"]' );
if ( ! button ) {
return;
}
button.trigger( 'click' );
$( '#edd_order_address_country' ).trigger( 'focus' );
},
/**
* Adds an `OrderItem` to `OrderItems`.
*
* @since 3.0
*
* @param {Object} e Submit event.
*/
onAdd( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
const { number } = state.get( 'formatters' );
state.set( 'isFetching', true );
// Use manual amounts if adjusting manually.
if ( true === model.get( '_isAdjustingManually' ) ) {
model.set( {
amount: number.unformat( model.get( 'amountManual' ) ),
tax: number.unformat( model.get( 'taxManual' ) ),
subtotal: number.unformat( model.get( 'subtotalManual' ) ),
} );
// Duplicate base amounts by the quantity set.
} else {
const quantity = model.get( 'quantity' );
model.set( {
tax: model.get( 'tax' ) * quantity,
subtotal: model.get( 'subtotal' ) * quantity,
} );
}
const items = state.get( 'items' );
// Add to collection but do not alert.
items.add( model, {
silent: true,
} );
// Update all amounts with new item and alert when done.
items
.updateAmounts()
.fail( ( { message: error } ) => {
// Remove added model on failure.
// It is is added previously to calculate Discounts
// as if adding would be successful.
items.remove( model, {
silent: true,
} );
// Clear fetching.
state.set( 'isFetching', false );
// Set error.
model.set( 'error', error );
} )
.done( () => {
// Stop listening to the model in this view.
this.stopListening( model );
// Alert of succesful addition.
items.trigger( 'add', model );
// Clear fetching.
state.set( 'isFetching', false );
} );
},
} );

View File

@ -0,0 +1,50 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* NoOrderItems
*
* @since 3.0
*
* @class NoOrderItems
* @augments wp.Backbone.View
*/
export const NoOrderItems = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-no-items' ),
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = this.options;
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 4 : 3;
return {
...Base.prototype.prepare.apply( this, arguments ),
config: {
colspan,
},
};
},
} );

View File

@ -0,0 +1,118 @@
/* global _ */
/**
* Internal dependencies
*/
import { Base } from './base.js';
/**
* OrderAdjustment
*
* @since 3.0
*
* @class OrderAdjustment
* @augments wp.Backbone.View
*/
export const OrderAdjustment = Base.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
className: 'is-expanded',
/**
* @since 3.0
*/
events: {
'click .delete': 'onDelete',
},
initialize() {
Base.prototype.initialize.apply( this );
// Set template depending on type.
switch ( this.model.get( 'type' ) ) {
case 'credit':
case 'fee':
this.template = wp.template( 'edd-admin-order-adjustment' );
break;
default:
this.template = wp.template(
'edd-admin-order-adjustment-discount'
);
}
// Listen for events.
this.listenTo( this.model, 'change', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
let orderItem;
if ( 'order_item' === model.get( 'objectType' ) ) {
orderItem = _.first( state.get( 'items' ).filter( ( item ) => {
return undefined !== item.get( 'adjustments' ).findWhere( {
objectId: item.get( 'id' ),
} );
} ) );
}
const subtotal = model.getAmount();
const total = model.getTotal();
return {
...Base.prototype.prepare.apply( this, arguments ),
config: {
colspan,
},
total,
subtotal,
orderItem: orderItem ? orderItem.toJSON() : false,
totalCurrency: currency.format( total ),
subtotalCurrency: currency.format( subtotal )
};
},
/**
* Removes the current Adjustment from Adjustments.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onDelete( e ) {
e.preventDefault();
const { state } = this.options;
// Remove `OrderAdjustment`.
state.get( 'adjustments' ).remove( this.model );
// Update `OrderItem` amounts.
state.get( 'items' ).updateAmounts();
},
} );

View File

@ -0,0 +1,101 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustment } from './order-adjustment.js';
/**
* OrderAdjustments
*
* @since 3.0
*
* @class OrderAdjustments
* @augments wp.Backbone.View
*/
export const OrderAdjustments = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__adjustments',
/**
* @since 3.0
*/
initialize() {
const { state } = this.options;
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( items, 'change', this.render );
this.listenTo( adjustments, 'add', this.render );
this.listenTo( adjustments, 'remove', this.remove );
},
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const adjustments = this.getAdjustments();
this.views.remove();
_.each( adjustments, ( adjustment ) => this.add( adjustment ) );
},
/**
* Adds an `OrderAdjustment` subview.
*
* @since 3.0
*
* @param {OrderAdjustment} model OrderAdjustment to add to view.
*/
add( model ) {
this.views.add(
new OrderAdjustment( {
...this.options,
model,
} )
);
},
/**
* Removes an `OrderAdjustment` subview.
*
* @since 3.0
*
* @param {OrderAdjustment} model OrderAdjustment to remove from view.
*/
remove( model ) {
let subview = null;
const views = this.views.get();
if ( ! views ) {
return;
}
// Find the Subview containing the model.
views.forEach( ( view ) => {
const { model: viewModel } = view;
if ( viewModel.id === model.id ) {
subview = view;
}
} );
// Remove Subview if found.
if ( null !== subview ) {
subview.remove();
}
},
} );

View File

@ -0,0 +1,27 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './order-adjustments.js';
/**
* OrderCredits
*
* @since 3.0
*
* @class OrderCredits
* @augments wp.Backbone.View
*/
export const OrderCredits = OrderAdjustments.extend( {
/**
* Returns Credit adjustments.
*
* @since 3.0.0
*/
getAdjustments() {
const { state } = this.options;
return state.get( 'adjustments' ).getByType( 'credit' );
},
} );

View File

@ -0,0 +1,31 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderAdjustments } from './order-adjustments.js';
/**
* OrderDiscountsFees
*
* @since 3.0
*
* @class OrderDiscountsFees
* @augments wp.Backbone.View
*/
export const OrderDiscountsFees = OrderAdjustments.extend( {
/**
* Returns Discount and Fee adjustments.
*
* @since 3.0.0
*/
getAdjustments() {
const { state } = this.options;
return state.get( 'adjustments' ).filter(
( adjustment ) => {
return [ 'discount', 'fee' ].includes( adjustment.get( 'type' ) );
}
);
},
} );

View File

@ -0,0 +1,127 @@
/**
* Internal dependencies
*/
import { Base } from './base.js';
import { CopyDownloadLink } from './copy-download-link.js';
/**
* OrderItem
*
* @since 3.0
*
* @class OrderItem
* @augments wp.Backbone.View
*/
export const OrderItem = Base.extend( {
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-item' ),
/**
* @since 3.0
*/
events: {
'click .delete': 'onDelete',
'click .copy-download-link': 'onCopyDownloadLink',
},
/**
* "Order Item" view.
*
* @since 3.0
*
* @constructs OrderItem
* @augments Base
*/
initialize() {
// Listen for events.
this.listenTo( this.model, 'change', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { currency } = state.get( 'formatters' );
const subtotal = model.getSubtotal();
const discountAmount = model.getDiscountAmount();
const isAdjustingManually = model.get( '_isAdjustingManually' );
const tax = model.getTax();
return {
...Base.prototype.prepare.apply( this, arguments ),
discount: discountAmount,
amountCurrency: currency.format( model.get( 'amount' ) ),
subtotal,
subtotalCurrency: currency.format( subtotal ),
tax,
taxCurrency: currency.format( tax ),
total: model.getTotal(),
config: {
isAdjustingManually,
},
adjustments: model.get( 'adjustments' ).toJSON(),
};
},
/**
* Removes the current Item from Items.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onDelete( e ) {
e.preventDefault();
const { model, options } = this;
const { state } = options;
// Remove OrderItem.
state.get( 'items' ).remove( model );
// Update remaining OrderItem amounts.
state.get( 'items' ).updateAmounts();
},
/**
* Opens a Dialog that fetches Download File URLs.
*
* @since 3.0
*
* @param {Object} e Click event.
*/
onCopyDownloadLink( e ) {
e.preventDefault();
const { options, model } = this;
new CopyDownloadLink( {
orderId: model.get( 'orderId' ),
productId: model.get( 'productId' ),
priceId: model.get( 'priceId' ),
} )
.openDialog()
.render();
},
} );

View File

@ -0,0 +1,120 @@
/* global _ */
/**
* Internal dependencies
*/
import { OrderItem } from './order-item.js';
import { NoOrderItems } from './no-order-items.js';
/**
* OrderItems
*
* @since 3.0
*
* @class OrderItems
* @augments wp.Backbone.View
*/
export const OrderItems = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__items',
/**
* "Order Items" view.
*
* @since 3.0
*
* @constructs OrderItem
* @augments Base
*/
initialize() {
const { state } = this.options;
const items = state.get( 'items' );
const adjustments = state.get( 'adjustments' );
// Listen for events.
this.listenTo( items, 'add', this.render );
this.listenTo( items, 'remove', this.remove );
},
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const { state } = this.options;
const items = state.get( 'items' );
this.views.remove();
// Nothing available.
if ( 0 === items.length ) {
this.views.set(
new NoOrderItems( {
...this.options,
} )
);
// Render each item.
} else {
_.each( items.models, ( model ) => this.add( model ) );
}
},
/**
* Adds an `OrderItem` subview.
*
* @since 3.0
*
* @param {OrderItem} model OrderItem
*/
add( model ) {
this.views.add(
new OrderItem( {
...this.options,
model,
} )
);
},
/**
* Removes an `OrderItem` subview.
*
* @since 3.0
*
* @param {OrderItem} model OrderItem
*/
remove( model ) {
let subview = null;
// Find the subview containing the model.
this.views.get().forEach( ( view ) => {
const { model: viewModel } = view;
if ( viewModel.get( 'id' ) === model.id ) {
subview = view;
}
} );
// Remove subview if found.
if ( null !== subview ) {
subview.remove();
}
// Last item was removed, show "No items".
if ( 0 === this.views.get().length ) {
this.views.set(
new NoOrderItems( {
...this.options,
} )
);
}
},
} );

View File

@ -0,0 +1,57 @@
/* global wp */
/**
* OrderRefund
*
* @since 3.0
*
* @class OrderRefund
* @augments wp.Backbone.View
*/
export const OrderRefund = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-refund' ),
/**
* @since 3.0
*/
tagName: 'tr',
/**
* @since 3.0
*/
className: 'is-expanded',
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { model, options } = this;
const { state } = options;
const { currency } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
return {
config: {
colspan,
},
id: model.get( 'id' ),
number: model.get( 'number' ),
dateCreated: model.get( 'dateCreatedi18n' ),
totalCurrency: currency.format( model.get( 'total' ) ),
};
},
} );

View File

@ -0,0 +1,48 @@
/**
* Internal dependencies
*/
import { OrderRefund } from './order-refund.js';
/**
* Order refunds
*
* @since 3.0
*
* @class OrderRefunds
* @augments wp.Backbone.View
*/
export const OrderRefunds = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__refunds',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-refunds' ),
/**
* Renders initial view.
*
* @since 3.0
*/
render() {
const { state } = this.options;
const { models: refunds } = state.get( 'refunds' );
_.each( refunds, ( model ) => (
this.views.add(
new OrderRefund( {
...this.options,
model,
} )
)
) );
},
} );

View File

@ -0,0 +1,68 @@
/**
* Order subtotal
*
* @since 3.0
*
* @class Subtotal
* @augments wp.Backbone.View
*/
export const OrderSubtotal = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__subtotal',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-subtotal' ),
/**
* Order subtotal view.
*
* @since 3.0
*
* @constructs OrderSubtotal
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const subtotal = state.getSubtotal();
return {
state: state.toJSON(),
config: {
colspan,
},
subtotal,
subtotalCurrency: currency.format( subtotal ),
};
},
} );

View File

@ -0,0 +1,130 @@
/**
* Order tax
*
* @since 3.0
*
* @class OrderTax
* @augments wp.Backbone.View
*/
export const OrderTax = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__tax',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-tax' ),
/**
* @since 3.0
*/
events: {
'click #notice-tax-change .notice-dismiss': 'onDismissTaxRateChange',
'click #notice-tax-change .update-amounts': 'onUpdateAmounts',
},
/**
* Order total view.
*
* @since 3.0
*
* @constructs OrderTotal
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const tax = state.getTax();
const hasNewTaxRate = state.hasNewTaxRate();
const taxableItems = [
...state.get( 'items' ).models,
...state.get( 'adjustments' ).getByType( 'fee' ),
];
return {
state: {
...state.toJSON(),
hasNewTaxRate,
},
config: {
colspan,
},
tax,
taxCurrency: currency.format( tax ),
hasTaxableItems: taxableItems.length > 0,
};
},
/**
* Dismisses Tax Rate change notice.
*
* @since 3.0
*/
onDismissTaxRateChange() {
const { state } = this.options;
// Reset amount
state.set( 'hasTax', state.get( 'hasTax' ) );
// Manually trigger change because new and previous attributes
// are the same so Backbone will not.
state.trigger( 'change:hasTax' );
},
/**
* Updates amounts for existing Order Items.
*
* @since 3.0
*/
onUpdateAmounts( e ) {
e.preventDefault();
const { state } = this.options;
// Manually recalculate taxed fees.
state.get( 'adjustments' ).getByType( 'fee' ).forEach(
( fee ) => {
fee.updateTax();
}
);
// Request updated tax amounts for orders from the server.
state.get( 'items' )
.updateAmounts()
.done( () => {
this.onDismissTaxRateChange();
} );
},
} );

View File

@ -0,0 +1,81 @@
/**
* Order total
*
* @since 3.0
*
* @class OrderTotal
* @augments wp.Backbone.View
*/
export const OrderTotal = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
tagName: 'tbody',
/**
* @since 3.0
*/
className: 'edd-order-overview-summary__total',
/**
* @since 3.0
*/
template: wp.template( 'edd-admin-order-total' ),
/**
* Order tax view.
*
* @since 3.0
*
* @constructs OrderTax
* @augments wp.Backbone.View
*/
initialize() {
const { state } = this.options;
// Listen for events.
this.listenTo( state, 'change:hasTax', this.render );
this.listenTo( state.get( 'items' ), 'add remove change', this.render );
this.listenTo( state.get( 'adjustments' ), 'add remove', this.render );
},
/**
* Prepares data to be used in `render` method.
*
* @since 3.0
*
* @see wp.Backbone.View
* @see https://github.com/WordPress/WordPress/blob/master/wp-includes/js/wp-backbone.js
*
* @return {Object} The data for this view.
*/
prepare() {
const { state } = this.options;
const { currency, number } = state.get( 'formatters' );
// Determine column offset -- using cart quantities requires an extra column.
const colspan = true === state.get( 'hasQuantity' ) ? 2 : 1;
const total = state.getTotal();
const discount = state.getDiscount();
const hasManualAdjustment = undefined !== state.get( 'items' ).findWhere( {
_isAdjustingManually: true,
} );
return {
state: {
...state.toJSON(),
hasManualAdjustment,
},
config: {
colspan,
},
total,
discount,
discountCurrency: currency.format( discount ),
totalCurrency: currency.format( total ),
};
},
} );

View File

@ -0,0 +1,66 @@
/** global wp */
/**
* Internal dependencies
*/
import { Summary } from './summary.js';
import { Actions } from './actions.js';
/**
* Overview
*
* @since 3.0
*
* @class Overview
* @augments wp.Backbone.View
*/
export const Overview = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview',
/**
* @since 3.0
*/
events: {
'click .toggle-row': 'onToggleRow',
},
/**
* Renders the view.
*
* @since 3.0
*
* @return {Overview} Current view.
*/
render() {
// Add "Summary".
//
// Contains `OrderItems`, `OrderAdjustments`, and `Totals` subviews.
this.views.add( new Summary( this.options ) );
// "Actions".
if ( document.getElementById( 'edd-order-overview-actions' ) ) {
this.views.add( new Actions( this.options ) );
}
return this;
},
/**
* Toggles a row's other columns.
*
* Core does not support the dynamically added items.
*
* @since 3.0
*
* @see https://github.com/WordPress/WordPress/blob/001ffe81fbec4438a9f594f330e18103d21fbcd7/wp-admin/js/common.js#L908
*
* @param {Object} e Click event.
*/
onToggleRow( e ) {
e.preventDefault();
$( e.target ).closest( 'tr' ).toggleClass( 'is-expanded' );
},
} );

View File

@ -0,0 +1,46 @@
/** global wp */
/**
* Internal dependencies
*/
import { OrderItems } from './order-items.js';
import { OrderSubtotal } from './order-subtotal.js';
import { OrderDiscountsFees } from './order-discounts-fees.js';
import { OrderTax } from './order-tax.js';
import { OrderCredits } from './order-credits.js';
import { OrderTotal } from './order-total.js';
import { OrderRefunds } from './order-refunds.js';
/**
* Overview summary
*
* @since 3.0
*
* @class Summary
* @augments wp.Backbone.view
*/
export const Summary = wp.Backbone.View.extend( {
/**
* @since 3.0
*/
el: '#edd-order-overview-summary',
/**
* Renders the view.
*
* @since 3.0
*
* @return {Summary} Current view.
*/
render() {
this.views.add( new OrderItems( this.options ) );
this.views.add( new OrderSubtotal( this.options ) );
this.views.add( new OrderDiscountsFees( this.options ) );
this.views.add( new OrderTax( this.options ) );
this.views.add( new OrderCredits( this.options ) );
this.views.add( new OrderTotal( this.options ) );
this.views.add( new OrderRefunds( this.options ) );
return this;
},
} );