Merge branch 'master' into update/travis-git-depth
This commit is contained in:
commit
e1856b961d
|
@ -1,35 +0,0 @@
|
|||
engines:
|
||||
phpcodesniffer:
|
||||
enabled: true
|
||||
config:
|
||||
standard: "WordPress"
|
||||
eslint:
|
||||
enabled: true
|
||||
scss-lint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
- php
|
||||
- javascript
|
||||
ratings:
|
||||
paths:
|
||||
- "includes/*"
|
||||
exclude_paths:
|
||||
- "tests/"
|
||||
- "sample-data/"
|
||||
- "i18n/"
|
||||
- "includes/api/legacy/"
|
||||
- "includes/libraries/"
|
||||
- "includes/updates/"
|
||||
- "includes/shipping/legacy-*"
|
||||
- "includes/wc-deprecated-functions.php"
|
||||
- "assets/js/accounting/"
|
||||
- "assets/js/jquery-*"
|
||||
- "assets/js/prettyPhoto/"
|
||||
- "assets/js/round/"
|
||||
- "assets/js/select2/"
|
||||
- "assets/js/selectWoo/"
|
||||
- "assets/js/stupidtable/"
|
||||
- "assets/js/zeroclipboard/"
|
|
@ -34,7 +34,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Update nightly tag
|
||||
uses: richardsimko/github-tag-action@v1.0.4
|
||||
uses: richardsimko/github-tag-action@v1.0.5
|
||||
with:
|
||||
tag_name: nightly
|
||||
env:
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
tools:
|
||||
php_code_sniffer:
|
||||
config:
|
||||
standard: WordPress
|
||||
sensiolabs_security_checker: true
|
||||
external_code_coverage:
|
||||
timeout: 2500
|
||||
checks:
|
||||
php:
|
||||
avoid_closing_tag: false
|
||||
avoid_superglobals: false
|
||||
coding_standard:
|
||||
name: WordPress
|
||||
no_exit: false
|
||||
no_global_keyword: false
|
||||
one_class_per_file: false
|
||||
psr2_class_declaration: false
|
||||
psr2_control_structure_declaration: false
|
||||
psr2_switch_declaration: false
|
||||
variable_existence: false
|
||||
verify_access_scope_valid: false
|
||||
verify_argument_usable_as_reference: false
|
||||
verify_property_names: false
|
||||
filter:
|
||||
excluded_paths:
|
||||
- sample-data/
|
||||
- i18n/
|
||||
- includes/api/legacy/
|
||||
- includes/legacy/
|
||||
- includes/libraries/
|
||||
- includes/shipping/legacy-*
|
||||
- includes/updates/
|
||||
- includes/vendor/
|
||||
- includes/wc-deprecated-functions.php
|
||||
- tests/
|
|
@ -70,10 +70,9 @@ install:
|
|||
fi
|
||||
- composer install
|
||||
- |
|
||||
# Install WP Test suite, install PHPUnit globally:
|
||||
# Install WP Test suite:
|
||||
if [[ ! -z "$WP_VERSION" ]]; then
|
||||
bash tests/bin/install.sh woocommerce_test root '' localhost $WP_VERSION
|
||||
composer global require "phpunit/phpunit=6.5.*|7.5.*"
|
||||
fi
|
||||
|
||||
script:
|
||||
|
@ -90,10 +89,12 @@ branches:
|
|||
- /^\d+\.\d+(\.\d+)?(-\S*)?$/
|
||||
- /^release\//
|
||||
|
||||
# Composer 2.0.7 introduced a change that broke the jetpack autoloader in PHP 7.0 - 7.3.
|
||||
before_install:
|
||||
- composer self-update --1
|
||||
- composer self-update 2.0.6
|
||||
|
||||
# Git clone depth
|
||||
# By default Travis CI clones repositories to a depth of 50 commits. Using a depth of 1 makes this step a bit faster.
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
<img src="https://img.shields.io/wordpress/plugin/dt/woocommerce.svg" alt="WordPress.org downloads">
|
||||
<img src="https://img.shields.io/wordpress/plugin/r/woocommerce.svg" alt="WordPress.org rating">
|
||||
<a href="https://travis-ci.com/woocommerce/woocommerce"><img src="https://travis-ci.com/woocommerce/woocommerce.svg?branch=master" alt="Build Status"></a>
|
||||
<a href="https://scrutinizer-ci.com/g/woocommerce/woocommerce/?branch=master"><img src="https://scrutinizer-ci.com/g/woocommerce/woocommerce/badges/quality-score.png?b=master" alt="Scrutinizer Code Quality"></a>
|
||||
<a href="https://codecov.io/gh/woocommerce/woocommerce"><img src="https://codecov.io/gh/woocommerce/woocommerce/branch/master/graph/badge.svg" alt="codecov"></a>
|
||||
</p>
|
||||
|
||||
|
|
|
@ -360,7 +360,7 @@
|
|||
}
|
||||
|
||||
.addons-button-solid {
|
||||
background-color: #955a89;
|
||||
background-color:#674399;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
@ -379,6 +379,16 @@
|
|||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.addons-button-outline-purple {
|
||||
border: 1px solid #674399;
|
||||
color: #674399;
|
||||
}
|
||||
|
||||
.addons-button-outline-purple:hover {
|
||||
color: #674399;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.addons-button-outline-white {
|
||||
border: 1px solid #fff;
|
||||
color: #fff;
|
||||
|
|
|
@ -1661,6 +1661,16 @@ a.reset_variations {
|
|||
}
|
||||
}
|
||||
|
||||
form[name="checkout"] {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.blockUI.blockOverlay {
|
||||
position: relative;
|
||||
|
||||
@include loader();
|
||||
}
|
||||
|
||||
form {
|
||||
|
||||
.col2-set {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
|
@ -19,7 +19,7 @@ jQuery( function ( $ ) {
|
|||
)
|
||||
) {
|
||||
/* State/Country select boxes */
|
||||
this.states = $.parseJSON( woocommerce_admin_meta_boxes_order.countries.replace( /"/g, '"' ) );
|
||||
this.states = JSON.parse( woocommerce_admin_meta_boxes_order.countries.replace( /"/g, '"' ) );
|
||||
}
|
||||
|
||||
$( '.js_field-country' ).selectWoo().change( this.change_country );
|
||||
|
|
|
@ -9,7 +9,7 @@ jQuery( function ( $ ) {
|
|||
init: function() {
|
||||
if ( typeof wc_users_params.countries !== 'undefined' ) {
|
||||
/* State/Country select boxes */
|
||||
this.states = $.parseJSON( wc_users_params.countries.replace( /"/g, '"' ) );
|
||||
this.states = JSON.parse( wc_users_params.countries.replace( /"/g, '"' ) );
|
||||
}
|
||||
|
||||
$( '.js_field-country' ).selectWoo().change( this.change_country );
|
||||
|
|
|
@ -6,7 +6,7 @@ jQuery( function( $ ) {
|
|||
return false;
|
||||
}
|
||||
|
||||
var locale_json = wc_address_i18n_params.locale.replace( /"/g, '"' ), locale = $.parseJSON( locale_json );
|
||||
var locale_json = wc_address_i18n_params.locale.replace( /"/g, '"' ), locale = JSON.parse( locale_json );
|
||||
|
||||
function field_is_required( field, is_required ) {
|
||||
if ( is_required ) {
|
||||
|
@ -51,7 +51,7 @@ jQuery( function( $ ) {
|
|||
$statefield.attr( 'data-o_class', $statefield.attr( 'class' ) );
|
||||
}
|
||||
|
||||
var locale_fields = $.parseJSON( wc_address_i18n_params.locale_fields );
|
||||
var locale_fields = JSON.parse( wc_address_i18n_params.locale_fields );
|
||||
|
||||
$.each( locale_fields, function( key, value ) {
|
||||
|
||||
|
|
|
@ -115,7 +115,7 @@ jQuery( function( $ ) {
|
|||
} );
|
||||
|
||||
try {
|
||||
var wc_fragments = $.parseJSON( sessionStorage.getItem( wc_cart_fragments_params.fragment_name ) ),
|
||||
var wc_fragments = JSON.parse( sessionStorage.getItem( wc_cart_fragments_params.fragment_name ) ),
|
||||
cart_hash = sessionStorage.getItem( cart_hash_key ),
|
||||
cookie_hash = Cookies.get( 'woocommerce_cart_hash'),
|
||||
cart_created = sessionStorage.getItem( 'wc_cart_created' );
|
||||
|
|
|
@ -191,7 +191,7 @@ jQuery( function( $ ) {
|
|||
},
|
||||
is_valid_json: function( raw_json ) {
|
||||
try {
|
||||
var json = $.parseJSON( raw_json );
|
||||
var json = JSON.parse( raw_json );
|
||||
|
||||
return ( json && 'object' === typeof json );
|
||||
} catch ( e ) {
|
||||
|
@ -507,9 +507,8 @@ jQuery( function( $ ) {
|
|||
$.ajax({
|
||||
type: 'POST',
|
||||
url: wc_checkout_params.checkout_url,
|
||||
data: new FormData( this ),
|
||||
contentType: false,
|
||||
processData: false,
|
||||
data: $form.serialize(),
|
||||
dataType: 'json',
|
||||
success: function( result ) {
|
||||
// Detach the unload handler that prevents a reload / redirect
|
||||
wc_checkout_form.detachUnloadEventsOnSubmit();
|
||||
|
|
|
@ -77,7 +77,7 @@ jQuery( function( $ ) {
|
|||
|
||||
/* State/Country select boxes */
|
||||
var states_json = wc_country_select_params.countries.replace( /"/g, '"' ),
|
||||
states = $.parseJSON( states_json ),
|
||||
states = JSON.parse( states_json ),
|
||||
wrapper_selectors = '.woocommerce-billing-fields,' +
|
||||
'.woocommerce-shipping-fields,' +
|
||||
'.woocommerce-address-fields,' +
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
const { e2eBabelConfig } = require( '@woocommerce/e2e-environment' );
|
||||
|
||||
module.exports = function( api ) {
|
||||
api.cache( true );
|
||||
|
||||
return {
|
||||
...e2eBabelConfig,
|
||||
};
|
||||
};
|
|
@ -4,7 +4,7 @@
|
|||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1"
|
||||
"php": "7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ee5c0c106a076ca3b426771807c3ffeb",
|
||||
"content-hash": "c50f65dd9f9a26d397f7bb30228d7a88",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
|
@ -71,6 +71,10 @@
|
|||
"stylecheck",
|
||||
"tests"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
|
||||
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
|
||||
},
|
||||
"time": "2020-06-25T14:57:39+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -129,6 +133,10 @@
|
|||
"phpcs",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
|
||||
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
|
||||
},
|
||||
"time": "2019-12-27T09:44:58+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -181,6 +189,10 @@
|
|||
"polyfill",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
|
||||
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
|
||||
},
|
||||
"time": "2019-11-04T15:17:54+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -231,6 +243,10 @@
|
|||
"standards",
|
||||
"wordpress"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
|
||||
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
|
||||
},
|
||||
"time": "2019-08-28T14:22:28+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -282,6 +298,11 @@
|
|||
"phpcs",
|
||||
"standards"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
|
||||
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
|
||||
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
|
||||
},
|
||||
"time": "2020-10-23T02:01:07+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -322,6 +343,10 @@
|
|||
"woocommerce",
|
||||
"wordpress"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/woocommerce/woocommerce-sniffs/issues",
|
||||
"source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master"
|
||||
},
|
||||
"time": "2020-08-06T18:23:45+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -368,6 +393,11 @@
|
|||
"standards",
|
||||
"wordpress"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
|
||||
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
|
||||
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
|
||||
},
|
||||
"time": "2020-05-13T23:57:56+00:00"
|
||||
}
|
||||
],
|
||||
|
@ -379,6 +409,7 @@
|
|||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.1"
|
||||
}
|
||||
"php": "7.0"
|
||||
},
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.20"
|
||||
"phpunit/phpunit": "6.5.14"
|
||||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1"
|
||||
"php": "7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,7 +6,7 @@
|
|||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1"
|
||||
"php": "7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f89bceee93cc1d38e71a45e4cbf4f4aa",
|
||||
"content-hash": "4d4f2befccefe100869d30305083672b",
|
||||
"packages": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
|
@ -67,6 +67,11 @@
|
|||
"po",
|
||||
"translation"
|
||||
],
|
||||
"support": {
|
||||
"email": "oom@oscarotero.com",
|
||||
"issues": "https://github.com/oscarotero/Gettext/issues",
|
||||
"source": "https://github.com/php-gettext/Gettext/tree/v4.8.2"
|
||||
},
|
||||
"time": "2019-12-02T10:21:14+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -128,6 +133,10 @@
|
|||
"translations",
|
||||
"unicode"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-gettext/Languages/issues",
|
||||
"source": "https://github.com/php-gettext/Languages/tree/2.6.0"
|
||||
},
|
||||
"time": "2019-11-13T10:30:21+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -173,6 +182,10 @@
|
|||
}
|
||||
],
|
||||
"description": "Peast is PHP library that generates AST for JavaScript code",
|
||||
"support": {
|
||||
"issues": "https://github.com/mck89/peast/issues",
|
||||
"source": "https://github.com/mck89/peast/tree/v1.11.0"
|
||||
},
|
||||
"time": "2020-10-09T15:12:13+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -219,6 +232,10 @@
|
|||
"mustache",
|
||||
"templating"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bobthecow/mustache.php/issues",
|
||||
"source": "https://github.com/bobthecow/mustache.php/tree/master"
|
||||
},
|
||||
"time": "2019-11-23T21:40:31+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -268,29 +285,33 @@
|
|||
"iri",
|
||||
"sockets"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/rmccue/Requests/issues",
|
||||
"source": "https://github.com/rmccue/Requests/tree/master"
|
||||
},
|
||||
"time": "2016-10-13T00:11:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v3.4.45",
|
||||
"version": "v3.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4"
|
||||
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/52140652ed31cee3dabd0c481b5577201fa769b4",
|
||||
"reference": "52140652ed31cee3dabd0c481b5577201fa769b4",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
|
||||
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.5.9|>=7.0.8"
|
||||
"php": ">=5.5.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.4-dev"
|
||||
"dev-master": "3.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
|
@ -317,7 +338,10 @@
|
|||
],
|
||||
"description": "Symfony Finder Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2020-09-02T16:06:40+00:00"
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/3.3"
|
||||
},
|
||||
"time": "2017-06-01T21:01:25+00:00"
|
||||
},
|
||||
{
|
||||
"name": "wp-cli/i18n-command",
|
||||
|
@ -374,6 +398,10 @@
|
|||
],
|
||||
"description": "Provides internationalization tools for WordPress projects.",
|
||||
"homepage": "https://github.com/wp-cli/i18n-command",
|
||||
"support": {
|
||||
"issues": "https://github.com/wp-cli/i18n-command/issues",
|
||||
"source": "https://github.com/wp-cli/i18n-command/tree/master"
|
||||
},
|
||||
"time": "2020-07-08T15:20:38+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -422,6 +450,9 @@
|
|||
],
|
||||
"description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)",
|
||||
"homepage": "https://github.com/mustangostang/spyc/",
|
||||
"support": {
|
||||
"source": "https://github.com/wp-cli/spyc/tree/autoload"
|
||||
},
|
||||
"time": "2017-04-25T11:26:20+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -472,6 +503,10 @@
|
|||
"cli",
|
||||
"console"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/wp-cli/php-cli-tools/issues",
|
||||
"source": "https://github.com/wp-cli/php-cli-tools/tree/master"
|
||||
},
|
||||
"time": "2018-09-04T13:28:00+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -534,6 +569,11 @@
|
|||
"cli",
|
||||
"wordpress"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://make.wordpress.org/cli/handbook/",
|
||||
"issues": "https://github.com/wp-cli/wp-cli/issues",
|
||||
"source": "https://github.com/wp-cli/wp-cli"
|
||||
},
|
||||
"time": "2020-02-18T08:15:37+00:00"
|
||||
}
|
||||
],
|
||||
|
@ -545,6 +585,7 @@
|
|||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.1"
|
||||
}
|
||||
"php": "7.0"
|
||||
},
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,64 @@
|
|||
== Changelog ==
|
||||
|
||||
= 4.7.0 - 2020-11-10 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Tweak - Update `product_cat/tag` taxonomy template file names to `product-cat/tag`. #27736
|
||||
* Tweak - Exclude draft pages from the "Shop page" setting. #27890
|
||||
* Tweak - Styling to properly display product reviews within the dashboard activity widget. #27968
|
||||
* Fix - Fixes Photoswipe action buttons being obscured by admin bar. #27010
|
||||
* Fix - Allow variation image to be removed via REST API. #27299
|
||||
* Fix - Fixed WP CLI command to delete tax classes. #27310
|
||||
* Fix - Prevent regenerate image filter loop. #27483
|
||||
* Fix - Fixed some race conditions in `WC_Install`. #27696
|
||||
* Fix - Improved PHP 8 support for `Automattic\WooCommerce\RestApi\Utilities\SingletonTrait`. #27707
|
||||
* Fix - Adjust stock even if `reduce_stock` meta is not set in `wc_maybe_reduce_stock_levels`. #27763
|
||||
* Fix - Removed duplicated CSS code from jQuery UI. #27767
|
||||
* Fix - HTML syntax error in scheduled product message. #27842
|
||||
* Fix - Update logic to determine if an order requires payment to check the order instead of the cart. #27893
|
||||
* Fix - Use `Set password` title for lost password reset form when applicable. #27898
|
||||
* Fix - REST API - Fixed deprecated notices while querying orders and refunds through REST API v1 endpoints. #27934
|
||||
* Fix - Email address starting with `www` being displayed as a URL link in the admin order details page. #27983
|
||||
* Fix - Unexpected HTTP 401 "Sorry, you cannot list resources" REST API responses that occur when a plugin or custom code determines the current WordPress user before WooCommerce is fully initialized. #27587
|
||||
* Dev - Add `woocommerce_should_send_low_stock_notification` filter. #27819
|
||||
* Dev - Introduce (again) a dependency injection framework for the code in the src directory. #27733
|
||||
* Dev - Remove leftover code and data from the reverted improvement for variations filtering by attribute. #27748
|
||||
* Dev - Escaped labels in `woocommerce_form_field()`. #27800
|
||||
* Dev - Add a `NumberUtil::round` method to workaround a breaking change in the buil-in round function in PHP8. #27830
|
||||
* Dev - Remove default value from optional parameters that are followed by required parameters in functions/methods, since those are de-facto required and trigger a deprectation notice in PHP 8. #27840
|
||||
* Dev - REST API - Add user-friendly attribute names and values to order line items metadata.
|
||||
* Dev - REST API - Adds `parent_name` to `line_items` of the GET /orders endpoint.
|
||||
* Localization - Added Serbia districts. #27778
|
||||
* Localization - Make city, and postcode non-required fields. #27779
|
||||
* Localization - Add i18n locale information for Uganda, Kenya and Tanzania. #27164
|
||||
* Localization - Renamed "Postcode / ZIP" to "Pin code", and renamed "State / County" to "State" for India. #27516
|
||||
* Localization - Added postcode validation for addresses in India. #27546
|
||||
|
||||
**WooCommerce Blocks - 3.5.0 & 3.6.0**
|
||||
|
||||
* Make 'retry' property on errors from checkoutAfterProcessingWithSuccess/Error observers default to true if it's undefined. ([3261](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3261))
|
||||
* Ensure new payment methods are only displayed when no saved payment method is selected. ([3247](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3247))
|
||||
* Load WC Blocks CSS after editor CSS. ([3219](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3219))
|
||||
* Restore saved payment method data after closing an express payment method. ([3210](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3210))
|
||||
* Use light default background colour for country/state dropdowns. ([3189](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3189))
|
||||
* Fix broken Express Payment Method use in the Checkout block for logged out or incognito users. ([3165](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3165))
|
||||
* Fix State label for Spain. ([3147](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3147))
|
||||
* Don't throw an error when registering a payment method fails. ([3134](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3134))
|
||||
* Don't load contents of payment method hidden tabs. ([3227](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3227))
|
||||
* Use noticeContexts from useEmitResponse instead of hardcoded values. ([3161](https://github.com/woocommerce/woocommerce-gutenberg-products-block/pull/3161))
|
||||
|
||||
**WooCommerce Admin - 1.6.3**
|
||||
|
||||
* Tweak: Add BR and IN to list of stripe countries [#5377](https://github.com/woocommerce/woocommerce-admin/pull/5377)
|
||||
* Fix: Redirect instead of stalling on WCPay Inbox note action [#5413](https://github.com/woocommerce/woocommerce-admin/pull/5413)
|
||||
|
||||
= 4.6.2 - 2020-11-05 =
|
||||
|
||||
**WooCommerce**
|
||||
|
||||
* Prevent checkout from creating accounts when related setting is disabled.
|
||||
|
||||
= 4.6.1 - 2020-10-21 =
|
||||
|
||||
**WooCommerce**
|
||||
|
|
|
@ -10,13 +10,13 @@
|
|||
"php": ">=7.0",
|
||||
"automattic/jetpack-autoloader": "2.2.0",
|
||||
"automattic/jetpack-constants": "1.5.0",
|
||||
"composer/installers": "1.7.0",
|
||||
"composer/installers": "~1.7",
|
||||
"maxmind-db/reader": "1.6.0",
|
||||
"pelago/emogrifier": "3.1.0",
|
||||
"psr/container": "1.0.0",
|
||||
"woocommerce/action-scheduler": "3.1.6",
|
||||
"woocommerce/woocommerce-admin": "1.6.3",
|
||||
"woocommerce/woocommerce-blocks": "3.6.0",
|
||||
"woocommerce/woocommerce-admin": "1.7.0",
|
||||
"woocommerce/woocommerce-blocks": "3.8.0",
|
||||
"league/container": "3.3.3"
|
||||
},
|
||||
"require-dev": {
|
||||
|
@ -24,7 +24,7 @@
|
|||
},
|
||||
"config": {
|
||||
"platform": {
|
||||
"php": "7.1"
|
||||
"php": "7.0"
|
||||
},
|
||||
"preferred-install": {
|
||||
"woocommerce/action-scheduler": "dist",
|
||||
|
@ -90,10 +90,11 @@
|
|||
},
|
||||
"extra": {
|
||||
"installer-paths": {
|
||||
"packages/action-scheduler": ["woocommerce/action-scheduler"],
|
||||
"packages/woocommerce-rest-api": ["woocommerce/woocommerce-rest-api"],
|
||||
"packages/woocommerce-blocks": ["woocommerce/woocommerce-blocks"],
|
||||
"packages/woocommerce-admin": ["woocommerce/woocommerce-admin"]
|
||||
"packages/{$name}": [
|
||||
"woocommerce/action-scheduler",
|
||||
"woocommerce/woocommerce-blocks",
|
||||
"woocommerce/woocommerce-admin"
|
||||
]
|
||||
},
|
||||
"scripts-description": {
|
||||
"test": "Run unit tests",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "ed84c4d91482a5c508caaf50e843de58",
|
||||
"content-hash": "6494b4d4b956386e32381541ebd79839",
|
||||
"packages": [
|
||||
{
|
||||
"name": "automattic/jetpack-autoloader",
|
||||
|
@ -40,6 +40,9 @@
|
|||
"GPL-2.0-or-later"
|
||||
],
|
||||
"description": "Creates a custom autoloader for a plugin or theme.",
|
||||
"support": {
|
||||
"source": "https://github.com/Automattic/jetpack-autoloader/tree/master"
|
||||
},
|
||||
"time": "2020-08-14T20:34:36+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -71,32 +74,38 @@
|
|||
"GPL-2.0-or-later"
|
||||
],
|
||||
"description": "A wrapper for defining constants in a more testable way.",
|
||||
"support": {
|
||||
"source": "https://github.com/Automattic/jetpack-constants/tree/master"
|
||||
},
|
||||
"time": "2020-08-13T14:33:09+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/installers",
|
||||
"version": "v1.7.0",
|
||||
"version": "v1.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/installers.git",
|
||||
"reference": "141b272484481432cda342727a427dc1e206bfa0"
|
||||
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/installers/zipball/141b272484481432cda342727a427dc1e206bfa0",
|
||||
"reference": "141b272484481432cda342727a427dc1e206bfa0",
|
||||
"url": "https://api.github.com/repos/composer/installers/zipball/b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
|
||||
"reference": "b93bcf0fa1fccb0b7d176b0967d969691cd74cca",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "^1.0"
|
||||
"composer-plugin-api": "^1.0 || ^2.0"
|
||||
},
|
||||
"replace": {
|
||||
"roundcube/plugin-installer": "*",
|
||||
"shama/baton": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"composer/composer": "1.0.*@dev",
|
||||
"phpunit/phpunit": "^4.8.36"
|
||||
"composer/composer": "1.6.* || 2.0.*@dev",
|
||||
"composer/semver": "1.0.* || 2.0.*@dev",
|
||||
"phpunit/phpunit": "^4.8.36",
|
||||
"sebastian/comparator": "^1.2.4",
|
||||
"symfony/process": "^2.3"
|
||||
},
|
||||
"type": "composer-plugin",
|
||||
"extra": {
|
||||
|
@ -132,6 +141,7 @@
|
|||
"Kanboard",
|
||||
"Lan Management System",
|
||||
"MODX Evo",
|
||||
"MantisBT",
|
||||
"Mautic",
|
||||
"Maya",
|
||||
"OXID",
|
||||
|
@ -186,6 +196,7 @@
|
|||
"shopware",
|
||||
"silverstripe",
|
||||
"sydes",
|
||||
"sylius",
|
||||
"symfony",
|
||||
"typo3",
|
||||
"wordpress",
|
||||
|
@ -193,7 +204,21 @@
|
|||
"zend",
|
||||
"zikula"
|
||||
],
|
||||
"time": "2019-08-12T15:00:31+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/composer/installers/issues",
|
||||
"source": "https://github.com/composer/installers/tree/v1.9.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://packagist.com",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-04-07T06:57:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/container",
|
||||
|
@ -259,6 +284,10 @@
|
|||
"provider",
|
||||
"service"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/container/issues",
|
||||
"source": "https://github.com/thephpleague/container/tree/3.3.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/philipobenito",
|
||||
|
@ -325,6 +354,10 @@
|
|||
"geolocation",
|
||||
"maxmind"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
|
||||
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0"
|
||||
},
|
||||
"time": "2019-12-19T22:59:03+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -399,6 +432,10 @@
|
|||
"email",
|
||||
"pre-processing"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/MyIntervals/emogrifier/issues",
|
||||
"source": "https://github.com/MyIntervals/emogrifier"
|
||||
},
|
||||
"time": "2019-12-26T19:37:31+00:00"
|
||||
},
|
||||
{
|
||||
|
@ -448,26 +485,35 @@
|
|||
"container-interop",
|
||||
"psr"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/php-fig/container/issues",
|
||||
"source": "https://github.com/php-fig/container/tree/master"
|
||||
},
|
||||
"time": "2017-02-14T16:28:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/css-selector",
|
||||
"version": "v3.4.46",
|
||||
"version": "v3.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/css-selector.git",
|
||||
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33"
|
||||
"reference": "4d882dced7b995d5274293039370148e291808f2"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
|
||||
"reference": "da3d9da2ce0026771f5fe64cb332158f1bd2bc33",
|
||||
"url": "https://api.github.com/repos/symfony/css-selector/zipball/4d882dced7b995d5274293039370148e291808f2",
|
||||
"reference": "4d882dced7b995d5274293039370148e291808f2",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^5.5.9|>=7.0.8"
|
||||
"php": ">=5.5.9"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\CssSelector\\": ""
|
||||
|
@ -481,14 +527,14 @@
|
|||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Jean-François Simon",
|
||||
"email": "jeanfrancois.simon@sensiolabs.com"
|
||||
},
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
|
@ -496,21 +542,10 @@
|
|||
],
|
||||
"description": "Symfony CssSelector Component",
|
||||
"homepage": "https://symfony.com",
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2020-10-24T10:57:07+00:00"
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/css-selector/tree/master"
|
||||
},
|
||||
"time": "2017-05-01T15:01:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/action-scheduler",
|
||||
|
@ -545,30 +580,35 @@
|
|||
],
|
||||
"description": "Action Scheduler for WordPress and WooCommerce",
|
||||
"homepage": "https://actionscheduler.org/",
|
||||
"support": {
|
||||
"issues": "https://github.com/woocommerce/action-scheduler/issues",
|
||||
"source": "https://github.com/woocommerce/action-scheduler/tree/master"
|
||||
},
|
||||
"time": "2020-05-12T16:22:33+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-admin",
|
||||
"version": "1.6.3",
|
||||
"version": "1.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-admin.git",
|
||||
"reference": "3015abbda8657ef097b7763e4c941daa06dab6f7"
|
||||
"reference": "14dc0c78ce163ed0d5daf8f83765b65a76f61010"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/3015abbda8657ef097b7763e4c941daa06dab6f7",
|
||||
"reference": "3015abbda8657ef097b7763e4c941daa06dab6f7",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-admin/zipball/14dc0c78ce163ed0d5daf8f83765b65a76f61010",
|
||||
"reference": "14dc0c78ce163ed0d5daf8f83765b65a76f61010",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"automattic/jetpack-autoloader": "^2.2.0",
|
||||
"composer/installers": "1.7.0",
|
||||
"composer/installers": "^1.9.0",
|
||||
"php": ">=5.6|>=7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.20",
|
||||
"woocommerce/woocommerce-sniffs": "0.0.9"
|
||||
"suin/phpcs-psr4-sniff": "^2.2",
|
||||
"woocommerce/woocommerce-sniffs": "0.1.0"
|
||||
},
|
||||
"type": "wordpress-plugin",
|
||||
"extra": {
|
||||
|
@ -579,9 +619,6 @@
|
|||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"includes/"
|
||||
],
|
||||
"psr-4": {
|
||||
"Automattic\\WooCommerce\\Admin\\": "src/"
|
||||
}
|
||||
|
@ -592,29 +629,33 @@
|
|||
],
|
||||
"description": "A modern, javascript-driven WooCommerce Admin experience.",
|
||||
"homepage": "https://github.com/woocommerce/woocommerce-admin",
|
||||
"time": "2020-10-26T20:25:00+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
|
||||
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v1.7.0"
|
||||
},
|
||||
"time": "2020-11-11T22:56:39+00:00"
|
||||
},
|
||||
{
|
||||
"name": "woocommerce/woocommerce-blocks",
|
||||
"version": "v3.6.0",
|
||||
"version": "v3.8.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/woocommerce/woocommerce-gutenberg-products-block.git",
|
||||
"reference": "1046697451f5e8906e48f0a7532b7637c3ded108"
|
||||
"reference": "8b7d485ec8d26a6d5c9011dbdb49443cad9beee7"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/1046697451f5e8906e48f0a7532b7637c3ded108",
|
||||
"reference": "1046697451f5e8906e48f0a7532b7637c3ded108",
|
||||
"url": "https://api.github.com/repos/woocommerce/woocommerce-gutenberg-products-block/zipball/8b7d485ec8d26a6d5c9011dbdb49443cad9beee7",
|
||||
"reference": "8b7d485ec8d26a6d5c9011dbdb49443cad9beee7",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"automattic/jetpack-autoloader": "^2.0.0",
|
||||
"composer/installers": "1.7.0"
|
||||
"composer/installers": "^1.7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "6.5.14",
|
||||
"woocommerce/woocommerce-sniffs": "0.0.7"
|
||||
"woocommerce/woocommerce-sniffs": "0.1.0"
|
||||
},
|
||||
"type": "wordpress-plugin",
|
||||
"extra": {
|
||||
|
@ -639,7 +680,11 @@
|
|||
"gutenberg",
|
||||
"woocommerce"
|
||||
],
|
||||
"time": "2020-10-12T15:35:42+00:00"
|
||||
"support": {
|
||||
"issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues",
|
||||
"source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v3.8.0"
|
||||
},
|
||||
"time": "2020-11-10T15:07:11+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
|
@ -687,6 +732,10 @@
|
|||
"isolation",
|
||||
"tool"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
|
||||
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
|
||||
},
|
||||
"time": "2020-05-03T08:27:20+00:00"
|
||||
}
|
||||
],
|
||||
|
@ -700,7 +749,7 @@
|
|||
},
|
||||
"platform-dev": [],
|
||||
"platform-overrides": {
|
||||
"php": "7.1"
|
||||
"php": "7.0"
|
||||
},
|
||||
"plugin-api-version": "1.1.0"
|
||||
"plugin-api-version": "2.0.0"
|
||||
}
|
||||
|
|
|
@ -519,6 +519,50 @@ abstract class WC_Data {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to compute meta cache key. Different from WP Meta cache key in that meta data cached using this key also contains meta_id column.
|
||||
*
|
||||
* @since 4.7.0
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_meta_cache_key() {
|
||||
if ( ! $this->get_id() ) {
|
||||
wc_doing_it_wrong( 'get_meta_cache_key', 'ID needs to be set before fetching a cache key.', '4.7.0' );
|
||||
return false;
|
||||
}
|
||||
return self::generate_meta_cache_key( $this->get_id(), $this->cache_group );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache key from id and group.
|
||||
*
|
||||
* @since 4.7.0
|
||||
*
|
||||
* @param int|string $id Object ID.
|
||||
* @param string $cache_group Group name use to store cache. Whole group cache can be invalidated in one go.
|
||||
*
|
||||
* @return string Meta cache key.
|
||||
*/
|
||||
public static function generate_meta_cache_key( $id, $cache_group ) {
|
||||
return WC_Cache_Helper::get_cache_prefix( $cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $id ) . 'object_meta_' . $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime caches for raw meta data. This includes meta_id column as well, which is not included by default in WP meta data.
|
||||
*
|
||||
* @since 4.7.0
|
||||
*
|
||||
* @param array $raw_meta_data_collection Array of objects of { object_id => array( meta_row_1, meta_row_2, ... }.
|
||||
* @param string $cache_group Name of cache group.
|
||||
*/
|
||||
public static function prime_raw_meta_data_cache( $raw_meta_data_collection, $cache_group ) {
|
||||
foreach ( $raw_meta_data_collection as $object_id => $raw_meta_data_array ) {
|
||||
$cache_key = self::generate_meta_cache_key( $object_id, $cache_group );
|
||||
wp_cache_set( $cache_key, $raw_meta_data_array, $cache_group );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Meta Data from the database. Ignore any internal properties.
|
||||
* Uses it's own caches because get_metadata does not provide meta_ids.
|
||||
|
@ -540,7 +584,7 @@ abstract class WC_Data {
|
|||
|
||||
if ( ! empty( $this->cache_group ) ) {
|
||||
// Prefix by group allows invalidation by group until https://core.trac.wordpress.org/ticket/4476 is implemented.
|
||||
$cache_key = WC_Cache_Helper::get_cache_prefix( $this->cache_group ) . WC_Cache_Helper::get_cache_prefix( 'object_' . $this->get_id() ) . 'object_meta_' . $this->get_id();
|
||||
$cache_key = $this->get_meta_cache_key();
|
||||
}
|
||||
|
||||
if ( ! $force_read ) {
|
||||
|
@ -550,7 +594,9 @@ abstract class WC_Data {
|
|||
}
|
||||
}
|
||||
|
||||
$raw_meta_data = $cache_loaded ? $cached_meta : $this->data_store->read_meta( $this );
|
||||
// We filter the raw meta data again when loading from cache, in case we cached in an earlier version where filter conditions were different.
|
||||
$raw_meta_data = $cache_loaded ? $this->data_store->filter_raw_meta_data( $this, $cached_meta ) : $this->data_store->read_meta( $this );
|
||||
|
||||
if ( $raw_meta_data ) {
|
||||
foreach ( $raw_meta_data as $meta ) {
|
||||
$this->meta_data[] = new WC_Meta_Data(
|
||||
|
|
|
@ -370,9 +370,9 @@ class WC_Admin_Addons {
|
|||
|
||||
$defaults = array(
|
||||
'image' => WC()->plugin_url() . '/assets/images/wcs-extensions-banner-3x.png',
|
||||
'image_alt' => __( 'WooCommerce Services', 'woocommerce' ),
|
||||
'image_alt' => __( 'WooCommerce Shipping', 'woocommerce' ),
|
||||
'title' => __( 'Buy discounted shipping labels — then print them from your dashboard.', 'woocommerce' ),
|
||||
'description' => __( 'Integrate your store with USPS to buy discounted shipping labels, and print them directly from your WooCommerce dashboard. Powered by WooCommerce Services.', 'woocommerce' ),
|
||||
'description' => __( 'Integrate your store with USPS to buy discounted shipping labels, and print them directly from your WooCommerce dashboard. Powered by WooCommerce Shipping.', 'woocommerce' ),
|
||||
'button' => __( 'Free - Install now', 'woocommerce' ),
|
||||
'href' => $button_url,
|
||||
'logos' => array(),
|
||||
|
@ -383,7 +383,7 @@ class WC_Admin_Addons {
|
|||
$local_defaults = array(
|
||||
'image' => WC()->plugin_url() . '/assets/images/wcs-truck-banner-3x.png',
|
||||
'title' => __( 'Show Canada Post shipping rates', 'woocommerce' ),
|
||||
'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Services.', 'woocommerce' ),
|
||||
'description' => __( 'Display live rates from Canada Post at checkout to make shipping a breeze. Powered by WooCommerce Shipping.', 'woocommerce' ),
|
||||
'logos' => array_merge(
|
||||
$defaults['logos'],
|
||||
array(
|
||||
|
@ -440,7 +440,69 @@ class WC_Admin_Addons {
|
|||
self::output_button(
|
||||
$block_data['href'],
|
||||
$block_data['button'],
|
||||
'addons-button-outline-green'
|
||||
'addons-button-outline-purple'
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the outputting of the WooCommerce Pay banner block.
|
||||
*
|
||||
* @param object $block Block data.
|
||||
*/
|
||||
public static function output_wcpay_banner_block( $block = array() ) {
|
||||
$is_active = is_plugin_active( 'woocommerce-payments/woocommerce-payments.php' );
|
||||
$location = wc_get_base_location();
|
||||
|
||||
if (
|
||||
! in_array( $location['country'], array( 'US' ), true ) ||
|
||||
$is_active ||
|
||||
! current_user_can( 'install_plugins' ) ||
|
||||
! current_user_can( 'activate_plugins' )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$button_url = wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'install-addon' => 'woocommerce-payments',
|
||||
)
|
||||
),
|
||||
'install-addon_woocommerce-payments'
|
||||
);
|
||||
|
||||
$defaults = array(
|
||||
'image' => WC()->plugin_url() . '/assets/images/wcpayments-icon-secure.png',
|
||||
'image_alt' => __( 'WooCommerce Payments', 'woocommerce' ),
|
||||
'title' => __( 'Payments made simple, with no monthly fees — exclusively for WooCommerce stores.', 'woocommerce' ),
|
||||
'description' => __( 'Securely accept cards in your store. See payments, track cash flow into your bank account, and stay on top of disputes – right from your dashboard.', 'woocommerce' ),
|
||||
'button' => __( 'Free - Install now', 'woocommerce' ),
|
||||
'href' => $button_url,
|
||||
'logos' => array(),
|
||||
);
|
||||
|
||||
$block_data = array_merge( $defaults, $block );
|
||||
?>
|
||||
<div class="addons-wcs-banner-block">
|
||||
<div class="addons-wcs-banner-block-image">
|
||||
<img
|
||||
class="addons-img"
|
||||
src="<?php echo esc_url( $block_data['image'] ); ?>"
|
||||
alt="<?php echo esc_attr( $block_data['image_alt'] ); ?>"
|
||||
/>
|
||||
</div>
|
||||
<div class="addons-wcs-banner-block-content">
|
||||
<h1><?php echo esc_html( $block_data['title'] ); ?></h1>
|
||||
<p><?php echo esc_html( $block_data['description'] ); ?></p>
|
||||
<?php
|
||||
self::output_button(
|
||||
$block_data['href'],
|
||||
$block_data['button'],
|
||||
'addons-button-outline-purple'
|
||||
);
|
||||
?>
|
||||
</div>
|
||||
|
@ -477,6 +539,9 @@ class WC_Admin_Addons {
|
|||
case 'wcs_banner_block':
|
||||
self::output_wcs_banner_block( (array) $section );
|
||||
break;
|
||||
case 'wcpay_banner_block':
|
||||
self::output_wcpay_banner_block( (array) $section );
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -520,7 +585,7 @@ class WC_Admin_Addons {
|
|||
* @param string $plugin The plugin the button is promoting.
|
||||
*/
|
||||
public static function output_button( $url, $text, $style, $plugin = '' ) {
|
||||
$style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-green' : $style;
|
||||
$style = __( 'Free', 'woocommerce' ) === $text ? 'addons-button-outline-purple' : $style;
|
||||
$style = is_plugin_active( $plugin ) ? 'addons-button-installed' : $style;
|
||||
$text = is_plugin_active( $plugin ) ? __( 'Installed', 'woocommerce' ) : $text;
|
||||
$url = self::add_in_app_purchase_url_params( $url );
|
||||
|
@ -546,8 +611,18 @@ class WC_Admin_Addons {
|
|||
return;
|
||||
}
|
||||
|
||||
if ( isset( $_GET['install-addon'] ) && 'woocommerce-services' === $_GET['install-addon'] ) {
|
||||
self::install_woocommerce_services_addon();
|
||||
if ( isset( $_GET['install-addon'] ) ) {
|
||||
switch ( $_GET['install-addon'] ) {
|
||||
case 'woocommerce-services':
|
||||
self::install_woocommerce_services_addon();
|
||||
break;
|
||||
case 'woocommerce-payments':
|
||||
self::install_woocommerce_payments_addon();
|
||||
break;
|
||||
default:
|
||||
// Do nothing.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$sections = self::get_sections();
|
||||
|
@ -591,6 +666,26 @@ class WC_Admin_Addons {
|
|||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install WooCommerce Payments from the Extensions screens.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function install_woocommerce_payments_addon() {
|
||||
check_admin_referer( 'install-addon_woocommerce-payments' );
|
||||
|
||||
$wcpay_plugin_id = 'woocommerce-payments';
|
||||
$wcpay_plugin = array(
|
||||
'name' => __( 'WooCommerce Payments', 'woocommerce' ),
|
||||
'repo-slug' => 'woocommerce-payments',
|
||||
);
|
||||
|
||||
WC_Install::background_installer( $services_plugin_id, $wcpay_plugin );
|
||||
|
||||
wp_safe_redirect( remove_query_arg( array( 'install-addon', '_wpnonce' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should an extension be shown on the featured page.
|
||||
*
|
||||
|
|
|
@ -377,6 +377,11 @@ class WC_Admin_Status {
|
|||
private static function output_plugins_info( $plugins, $untested_plugins ) {
|
||||
$wc_version = Constants::get_constant( 'WC_VERSION' );
|
||||
|
||||
if ( 'major' === WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE ) {
|
||||
// Since we're only testing against major, we don't need to show minor and patch version.
|
||||
$wc_version = $wc_version[0] . '.0';
|
||||
}
|
||||
|
||||
foreach ( $plugins as $plugin ) {
|
||||
if ( ! empty( $plugin['name'] ) ) {
|
||||
// Link the plugin name to the plugin url if available.
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
use \Automattic\Jetpack\Constants;
|
||||
use \Automattic\WooCommerce\Admin\Notes\WC_Admin_Note;
|
||||
use Automattic\WooCommerce\Admin\Notes\Note;
|
||||
|
||||
/**
|
||||
* WC_Notes_Run_Db_Update.
|
||||
|
@ -58,7 +58,7 @@ class WC_Notes_Run_Db_Update {
|
|||
// Remove weird duplicates. Leave the first one.
|
||||
$current_notice = array_shift( $note_ids );
|
||||
foreach ( $note_ids as $note_id ) {
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note = new Note( $note_id );
|
||||
$data_store->delete( $note );
|
||||
}
|
||||
return $current_notice;
|
||||
|
@ -77,8 +77,8 @@ class WC_Notes_Run_Db_Update {
|
|||
return;
|
||||
}
|
||||
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
$note = new Note( $note_id );
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_ACTIONED );
|
||||
$note->save();
|
||||
}
|
||||
|
||||
|
@ -89,7 +89,7 @@ class WC_Notes_Run_Db_Update {
|
|||
* - actions are set up for the first 'Update database' notice, and
|
||||
* - URL for note's action is equal to the given URL (to check for potential nonce update).
|
||||
*
|
||||
* @param WC_Admin_Note $note Note to check.
|
||||
* @param Note $note Note to check.
|
||||
* @param string $update_url URL to check the note against.
|
||||
* @param array<int, string> $current_actions List of actions to check for.
|
||||
* @return bool
|
||||
|
@ -135,9 +135,9 @@ class WC_Notes_Run_Db_Update {
|
|||
);
|
||||
|
||||
if ( $note_id ) {
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note = new Note( $note_id );
|
||||
} else {
|
||||
$note = new WC_Admin_Note();
|
||||
$note = new Note();
|
||||
}
|
||||
|
||||
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
|
||||
|
@ -151,13 +151,13 @@ class WC_Notes_Run_Db_Update {
|
|||
/* translators: %1$s: opening <a> tag %2$s: closing </a> tag*/
|
||||
. sprintf( ' ' . esc_html__( 'The database update process runs in the background and may take a little while, so please be patient. Advanced users can alternatively update via %1$sWP CLI%2$s.', 'woocommerce' ), '<a href="https://github.com/woocommerce/woocommerce/wiki/Upgrading-the-database-using-WP-CLI">', '</a>' )
|
||||
);
|
||||
$note->set_type( WC_Admin_Note::E_WC_ADMIN_NOTE_UPDATE );
|
||||
$note->set_type( Note::E_WC_ADMIN_NOTE_UPDATE );
|
||||
$note->set_name( self::NOTE_NAME );
|
||||
$note->set_content_data( (object) array() );
|
||||
$note->set_source( 'woocommerce-core' );
|
||||
// In case db version is out of sync with WC version or during the next update, the notice needs to show up again,
|
||||
// so set it to unactioned.
|
||||
$note->set_status( WC_Admin_Note::E_WC_ADMIN_NOTE_UNACTIONED );
|
||||
$note->set_status( Note::E_WC_ADMIN_NOTE_UNACTIONED );
|
||||
|
||||
// Set new actions.
|
||||
$note->clear_actions();
|
||||
|
@ -181,7 +181,7 @@ class WC_Notes_Run_Db_Update {
|
|||
$cron_disabled = Constants::is_true( 'DISABLE_WP_CRON' );
|
||||
$cron_cta = $cron_disabled ? __( 'You can manually run queued updates here.', 'woocommerce' ) : __( 'View progress →', 'woocommerce' );
|
||||
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note = new Note( $note_id );
|
||||
$note->set_title( __( 'WooCommerce database update in progress', 'woocommerce' ) );
|
||||
$note->set_content( __( 'WooCommerce is updating the database in the background. The database update process may take a little while, so please be patient.', 'woocommerce' ) );
|
||||
|
||||
|
@ -227,7 +227,7 @@ class WC_Notes_Run_Db_Update {
|
|||
),
|
||||
);
|
||||
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note = new Note( $note_id );
|
||||
|
||||
// Check if the note needs to be updated (e.g. expired nonce or different note type stored in the previous run).
|
||||
if ( self::note_up_to_date( $note, $hide_notices_url, wp_list_pluck( $note_actions, 'name' ) ) ) {
|
||||
|
@ -266,7 +266,7 @@ class WC_Notes_Run_Db_Update {
|
|||
return;
|
||||
}
|
||||
|
||||
$note = new WC_Admin_Note( $note_id );
|
||||
$note = new Note( $note_id );
|
||||
if ( $note::E_WC_ADMIN_NOTE_ACTIONED === $note->get_status() ) {
|
||||
// Db update not needed && note actioned -> don't show it.
|
||||
return;
|
||||
|
|
|
@ -163,15 +163,6 @@ class WC_Plugin_Updates {
|
|||
$version .= '.' . $new_version_parts[1];
|
||||
}
|
||||
|
||||
if ( 'major' === $release ) {
|
||||
$current_version_parts = explode( '.', Constants::get_constant( 'WC_VERSION' ) );
|
||||
|
||||
// If user has already moved to the major version, we don't need to flag up anything.
|
||||
if ( version_compare( $current_version_parts[0] . '.' . $current_version_parts[1], $new_version_parts[0] . '.0', '>=' ) ) {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $extensions as $file => $plugin ) {
|
||||
if ( ! empty( $plugin[ self::VERSION_TESTED_HEADER ] ) ) {
|
||||
$plugin_version_parts = explode( '.', $plugin[ self::VERSION_TESTED_HEADER ] );
|
||||
|
|
|
@ -41,6 +41,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
|
||||
<?php if ( isset( $_GET['search'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
|
||||
<h1 class="search-form-title" >
|
||||
<?php // translators: search keyword. ?>
|
||||
<?php printf( esc_html__( 'Showing search results for: %s', 'woocommerce' ), '<strong>' . esc_html( sanitize_text_field( wp_unslash( $_GET['search'] ) ) ) . '</strong>' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
|
||||
</h1>
|
||||
<?php endif; ?>
|
||||
|
@ -71,6 +72,11 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
<?php WC_Admin_Addons::output_wcs_banner_block(); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( 'payment-gateways' === $current_section ) : ?>
|
||||
<div class="addons-shipping-methods">
|
||||
<?php WC_Admin_Addons::output_wcpay_banner_block(); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<ul class="products">
|
||||
<?php foreach ( $addons as $addon ) : ?>
|
||||
<?php
|
||||
|
|
|
@ -9,6 +9,11 @@ defined( 'ABSPATH' ) || exit;
|
|||
|
||||
global $wpdb;
|
||||
|
||||
if ( ! defined( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE' ) ) {
|
||||
// Define if we're checking against major or minor versions.
|
||||
define( 'WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE', 'major' );
|
||||
}
|
||||
|
||||
$report = wc()->api->get_endpoint_data( '/wc/v3/system_status' );
|
||||
$environment = $report['environment'];
|
||||
$database = $report['database'];
|
||||
|
@ -21,7 +26,7 @@ $security = $report['security'];
|
|||
$settings = $report['settings'];
|
||||
$wp_pages = $report['pages'];
|
||||
$plugin_updates = new WC_Plugin_Updates();
|
||||
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, 'minor' );
|
||||
$untested_plugins = $plugin_updates->get_untested_plugins( WC()->version, WC_SSR_PLUGIN_UPDATE_RELEASE_VERSION_TYPE );
|
||||
?>
|
||||
<div class="updated woocommerce-message inline">
|
||||
<p>
|
||||
|
|
|
@ -218,6 +218,14 @@ function wc_maybe_adjust_line_item_product_stock( $item, $item_quantity = -1 ) {
|
|||
$refunded_item_quantity = $order->get_qty_refunded_for_item( $item->get_id() );
|
||||
$diff = $item_quantity + $refunded_item_quantity - $already_reduced_stock;
|
||||
|
||||
/*
|
||||
* 0 as $item_quantity usually indicates we're deleting the order item.
|
||||
* We need to perform different calculations for this case.
|
||||
*/
|
||||
if ( 0 === $item_quantity ) {
|
||||
$diff = min( absint( $refunded_item_quantity ), $already_reduced_stock ) * -1;
|
||||
}
|
||||
|
||||
if ( $diff < 0 ) {
|
||||
$new_stock = wc_update_product_stock( $product, $diff * -1, 'increase' );
|
||||
} elseif ( $diff > 0 ) {
|
||||
|
|
|
@ -1572,10 +1572,15 @@ class WC_AJAX {
|
|||
$data_store = WC_Data_Store::load( 'product' );
|
||||
$ids = $data_store->search_products( $term, '', (bool) $include_variations, false, $limit, $include_ids, $exclude_ids );
|
||||
|
||||
$product_objects = array_filter( array_map( 'wc_get_product', $ids ), 'wc_products_array_filter_readable' );
|
||||
$products = array();
|
||||
$products = array();
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
$product_object = wc_get_product( $id );
|
||||
|
||||
if ( ! wc_products_array_filter_readable( $product_object ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ( $product_objects as $product_object ) {
|
||||
$formatted_name = $product_object->get_formatted_name();
|
||||
$managing_stock = $product_object->managing_stock();
|
||||
|
||||
|
|
|
@ -667,15 +667,18 @@ class WC_Checkout {
|
|||
* @return array of data.
|
||||
*/
|
||||
public function get_posted_data() {
|
||||
$skipped = array();
|
||||
$data = array(
|
||||
'terms' => (int) isset( $_POST['terms'] ), // WPCS: input var ok, CSRF ok.
|
||||
'createaccount' => (int) ! empty( $_POST['createaccount'] ), // WPCS: input var ok, CSRF ok.
|
||||
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
|
||||
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '', // WPCS: input var ok, CSRF ok.
|
||||
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(), // WPCS: input var ok, CSRF ok.
|
||||
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ), // WPCS: input var ok, CSRF ok.
|
||||
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
||||
$data = array(
|
||||
'terms' => (int) isset( $_POST['terms'] ),
|
||||
'createaccount' => (int) ( $this->is_registration_enabled() ? ! empty( $_POST['createaccount'] ) : false ),
|
||||
'payment_method' => isset( $_POST['payment_method'] ) ? wc_clean( wp_unslash( $_POST['payment_method'] ) ) : '',
|
||||
'shipping_method' => isset( $_POST['shipping_method'] ) ? wc_clean( wp_unslash( $_POST['shipping_method'] ) ) : '',
|
||||
'ship_to_different_address' => ! empty( $_POST['ship_to_different_address'] ) && ! wc_ship_to_billing_address_only(),
|
||||
'woocommerce_checkout_update_totals' => isset( $_POST['woocommerce_checkout_update_totals'] ),
|
||||
);
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
|
||||
$skipped = array();
|
||||
foreach ( $this->get_checkout_fields() as $fieldset_key => $fieldset ) {
|
||||
if ( $this->maybe_skip_fieldset( $fieldset_key, $data ) ) {
|
||||
$skipped[] = $fieldset_key;
|
||||
|
|
|
@ -906,12 +906,9 @@ class WC_Form_Handler {
|
|||
}
|
||||
|
||||
// Prevent parent variable product from being added to cart.
|
||||
if ( empty( $variation_id ) && $product->is_type( 'variable' ) ) {
|
||||
$url = get_permalink( $product_id );
|
||||
$product_name = $product->get_name();
|
||||
|
||||
/* translators: %1$s: Product link, %2$s: Product title, %3$s: Product name. */
|
||||
wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%3$s</a>.', 'woocommerce' ), esc_url( $url ), esc_html( $product_name ), esc_html( $product_name ) ), 'error' );
|
||||
if ( empty( $variation_id ) && $product && $product->is_type( 'variable' ) ) {
|
||||
/* translators: 1: product link, 2: product name */
|
||||
wc_add_notice( sprintf( __( 'Please choose product options by visiting <a href="%1$s" title="%2$s">%2$s</a>.', 'woocommerce' ), esc_url( get_permalink( $product_id ) ), esc_html( $product->get_name() ) ), 'error' );
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -308,7 +308,7 @@ class WC_Frontend_Scripts {
|
|||
}
|
||||
|
||||
/**
|
||||
* Register all WC sty;es.
|
||||
* Register all WC styles.
|
||||
*/
|
||||
private static function register_styles() {
|
||||
$version = Constants::get_constant( 'WC_VERSION' );
|
||||
|
|
|
@ -220,7 +220,7 @@ class WC_Shortcodes {
|
|||
|
||||
foreach ( $product_categories as $category ) {
|
||||
wc_get_template(
|
||||
'content-product_cat.php',
|
||||
'content-product-cat.php',
|
||||
array(
|
||||
'category' => $category,
|
||||
)
|
||||
|
@ -230,7 +230,7 @@ class WC_Shortcodes {
|
|||
woocommerce_product_loop_end();
|
||||
}
|
||||
|
||||
woocommerce_reset_loop();
|
||||
wc_reset_loop();
|
||||
|
||||
return '<div class="woocommerce columns-' . $columns . '">' . ob_get_clean() . '</div>';
|
||||
}
|
||||
|
|
|
@ -56,6 +56,9 @@ class WC_Validation {
|
|||
case 'BA':
|
||||
$valid = (bool) preg_match( '/^([7-8]{1})([0-9]{4})$/', $postcode );
|
||||
break;
|
||||
case 'BE':
|
||||
$valid = (bool) preg_match( '/^([0-9]{4})$/i', $postcode );
|
||||
break;
|
||||
case 'BR':
|
||||
$valid = (bool) preg_match( '/^([0-9]{5})([-])?([0-9]{3})$/', $postcode );
|
||||
break;
|
||||
|
|
|
@ -22,7 +22,7 @@ final class WooCommerce {
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
public $version = '4.8.0';
|
||||
public $version = '4.9.0';
|
||||
|
||||
/**
|
||||
* WooCommerce Schema version.
|
||||
|
|
|
@ -93,14 +93,13 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
|
|||
/**
|
||||
* Method to read an order from the database.
|
||||
*
|
||||
* @param WC_Data $order Order object.
|
||||
* @param WC_Order $order Order object.
|
||||
*
|
||||
* @throws Exception If passed order is invalid.
|
||||
*/
|
||||
public function read( &$order ) {
|
||||
$order->set_defaults();
|
||||
$post_object = get_post( $order->get_id() );
|
||||
|
||||
if ( ! $order->get_id() || ! $post_object || ! in_array( $post_object->post_type, wc_get_order_types(), true ) ) {
|
||||
throw new Exception( __( 'Invalid order.', 'woocommerce' ) );
|
||||
}
|
||||
|
@ -108,8 +107,8 @@ abstract class Abstract_WC_Order_Data_Store_CPT extends WC_Data_Store_WP impleme
|
|||
$order->set_props(
|
||||
array(
|
||||
'parent_id' => $post_object->post_parent,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
)
|
||||
);
|
||||
|
|
|
@ -123,8 +123,8 @@ class WC_Coupon_Data_Store_CPT extends WC_Data_Store_WP implements WC_Coupon_Dat
|
|||
array(
|
||||
'code' => $post_object->post_title,
|
||||
'description' => $post_object->post_excerpt,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'date_expires' => metadata_exists( 'post', $coupon_id, 'date_expires' ) ? get_post_meta( $coupon_id, 'date_expires', true ) : get_post_meta( $coupon_id, 'expiry_date', true ), // @todo: Migrate expiry_date meta to date_expires in upgrade routine.
|
||||
'discount_type' => get_post_meta( $coupon_id, 'discount_type', true ),
|
||||
'amount' => get_post_meta( $coupon_id, 'coupon_amount', true ),
|
||||
|
|
|
@ -152,10 +152,11 @@ class WC_Customer_Data_Store extends WC_Data_Store_WP implements WC_Customer_Dat
|
|||
|
||||
$customer_id = $customer->get_id();
|
||||
|
||||
// Load meta but exclude deprecated props.
|
||||
// Load meta but exclude deprecated props and parent keys.
|
||||
$user_meta = array_diff_key(
|
||||
array_change_key_case( array_map( 'wc_flatten_meta_callback', get_user_meta( $customer_id ) ) ),
|
||||
array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) )
|
||||
array_flip( array( 'country', 'state', 'postcode', 'city', 'address', 'address_2', 'default', 'location' ) ),
|
||||
array_change_key_case( (array) $user_object->data )
|
||||
);
|
||||
|
||||
$customer->set_props( $user_meta );
|
||||
|
|
|
@ -94,7 +94,20 @@ class WC_Data_Store_WP {
|
|||
$object->get_id()
|
||||
)
|
||||
);
|
||||
return $this->filter_raw_meta_data( $object, $raw_meta_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to filter internal meta keys from all meta data rows for the object.
|
||||
*
|
||||
* @since 4.7.0
|
||||
*
|
||||
* @param WC_Data $object WC_Data object.
|
||||
* @param array $raw_meta_data Array of std object of meta data to be filtered.
|
||||
*
|
||||
* @return mixed|void
|
||||
*/
|
||||
public function filter_raw_meta_data( &$object, $raw_meta_data ) {
|
||||
$this->internal_meta_keys = array_merge( array_map( array( $this, 'prefix_key' ), $object->get_data_keys() ), $this->internal_meta_keys );
|
||||
$meta_data = array_filter( $raw_meta_data, array( $this, 'exclude_internal_meta_keys' ) );
|
||||
return apply_filters( "woocommerce_data_store_wp_{$this->meta_type}_read_meta", $meta_data, $object, $this );
|
||||
|
@ -630,4 +643,16 @@ class WC_Data_Store_WP {
|
|||
);
|
||||
wp_cache_delete( 'lookup_table', 'object_' . $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WP post date string into a timestamp.
|
||||
*
|
||||
* @since 4.8.0
|
||||
*
|
||||
* @param string $time_string The WP post date string.
|
||||
* @return int|null The date string converted to a timestamp or null.
|
||||
*/
|
||||
protected function string_to_timestamp( $time_string ) {
|
||||
return '0000-00-00 00:00:00' !== $time_string ? wc_string_to_timestamp( $time_string ) : null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -734,11 +734,12 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
|
|||
* Get the order type based on Order ID.
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @param int $order_id Order ID.
|
||||
* @param int|WP_Post $order Order | Order id.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_order_type( $order_id ) {
|
||||
return get_post_type( $order_id );
|
||||
public function get_order_type( $order ) {
|
||||
return get_post_type( $order );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -865,7 +866,13 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
|
|||
$query = new WP_Query( $args );
|
||||
}
|
||||
|
||||
$orders = ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) ? $query->posts : array_filter( array_map( 'wc_get_order', $query->posts ) );
|
||||
if ( isset( $query_vars['return'] ) && 'ids' === $query_vars['return'] ) {
|
||||
$orders = $query->posts;
|
||||
} else {
|
||||
update_post_caches( $query->posts ); // We already fetching posts, might as well hydrate some caches.
|
||||
$order_ids = wp_list_pluck( $query->posts, 'ID' );
|
||||
$orders = $this->compile_orders( $order_ids, $query_vars, $query );
|
||||
}
|
||||
|
||||
if ( isset( $query_vars['paginate'] ) && $query_vars['paginate'] ) {
|
||||
return (object) array(
|
||||
|
@ -878,6 +885,213 @@ class WC_Order_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT implement
|
|||
return $orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile order response and set caches as needed for order ids.
|
||||
*
|
||||
* @param array $order_ids List of order IDS to compile.
|
||||
* @param array $query_vars Original query arguments.
|
||||
* @param WP_Query $query Query object.
|
||||
*
|
||||
* @return array Orders.
|
||||
*/
|
||||
private function compile_orders( $order_ids, $query_vars, $query ) {
|
||||
if ( empty( $order_ids ) ) {
|
||||
return array();
|
||||
}
|
||||
$orders = array();
|
||||
|
||||
// Lets do some cache hydrations so that we don't have to fetch data from DB for every order.
|
||||
$this->prime_raw_meta_cache_for_orders( $order_ids, $query_vars );
|
||||
$this->prime_refund_caches_for_order( $order_ids, $query_vars );
|
||||
$this->prime_order_item_caches_for_orders( $order_ids, $query_vars );
|
||||
|
||||
foreach ( $query->posts as $post ) {
|
||||
$orders[] = wc_get_order( $post );
|
||||
}
|
||||
|
||||
return $orders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime refund cache for orders.
|
||||
*
|
||||
* @param array $order_ids Order Ids to prime cache for.
|
||||
* @param array $query_vars Query vars for the query.
|
||||
*/
|
||||
private function prime_refund_caches_for_order( $order_ids, $query_vars ) {
|
||||
if ( ! isset( $query_vars['type'] ) || ! ( 'shop_order' === $query_vars['type'] ) ) {
|
||||
return;
|
||||
}
|
||||
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
|
||||
if ( is_array( $query_vars['fields'] ) && ! in_array( 'refunds', $query_vars['fields'] ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$cache_keys_mapping = array();
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$cache_keys_mapping[ $order_id ] = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $order_id;
|
||||
}
|
||||
$non_cached_ids = array();
|
||||
$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
|
||||
$non_cached_ids[] = $order_id;
|
||||
}
|
||||
}
|
||||
if ( empty( $non_cached_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$refunds = wc_get_orders(
|
||||
array(
|
||||
'type' => 'shop_order_refund',
|
||||
'post_parent__in' => $non_cached_ids,
|
||||
'limit' => - 1,
|
||||
)
|
||||
);
|
||||
$order_refunds = array_reduce(
|
||||
$refunds,
|
||||
function ( $order_refunds_array, WC_Order_Refund $refund ) {
|
||||
if ( ! isset( $order_refunds_array[ $refund->get_parent_id() ] ) ) {
|
||||
$order_refunds_array[ $refund->get_parent_id() ] = array();
|
||||
}
|
||||
$order_refunds_array[ $refund->get_parent_id() ][] = $refund;
|
||||
return $order_refunds_array;
|
||||
},
|
||||
array()
|
||||
);
|
||||
foreach ( $non_cached_ids as $order_id ) {
|
||||
$refunds = array();
|
||||
if ( isset( $order_refunds[ $order_id ] ) ) {
|
||||
$refunds = $order_refunds[ $order_id ];
|
||||
}
|
||||
wp_cache_set( $cache_keys_mapping[ $order_id ], $refunds, 'orders' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime following caches:
|
||||
* 1. item-$order_item_id For individual items.
|
||||
* 2. order-items-$order-id For fetching items associated with an order.
|
||||
* 3. order-item meta.
|
||||
*
|
||||
* @param array $order_ids Order Ids to prime cache for.
|
||||
* @param array $query_vars Query vars for the query.
|
||||
*/
|
||||
private function prime_order_item_caches_for_orders( $order_ids, $query_vars ) {
|
||||
global $wpdb;
|
||||
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
|
||||
$line_items = array(
|
||||
'line_items',
|
||||
'shipping_lines',
|
||||
'fee_lines',
|
||||
'coupon_lines',
|
||||
);
|
||||
|
||||
if ( is_array( $query_vars['fields'] ) && 0 === count( array_intersect( $line_items, $query_vars['fields'] ) ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$cache_keys = array_map(
|
||||
function ( $order_id ) {
|
||||
return 'order-items-' . $order_id;
|
||||
},
|
||||
$order_ids
|
||||
);
|
||||
$cache_values = wc_cache_get_multiple( $cache_keys, 'orders' );
|
||||
$non_cached_ids = array();
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
if ( false === $cache_values[ 'order-items-' . $order_id ] ) {
|
||||
$non_cached_ids[] = $order_id;
|
||||
}
|
||||
}
|
||||
if ( empty( $non_cached_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$non_cached_ids = esc_sql( $non_cached_ids );
|
||||
$non_cached_ids_string = implode( ',', $non_cached_ids );
|
||||
$order_items = $wpdb->get_results(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT order_item_type, order_item_id, order_id, order_item_name FROM {$wpdb->prefix}woocommerce_order_items WHERE order_id in ( $non_cached_ids_string ) ORDER BY order_item_id;"
|
||||
);
|
||||
if ( empty( $order_items ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order_items_for_all_orders = array_reduce(
|
||||
$order_items,
|
||||
function ( $order_items_collection, $order_item ) {
|
||||
if ( ! isset( $order_items_collection[ $order_item->order_id ] ) ) {
|
||||
$order_items_collection[ $order_item->order_id ] = array();
|
||||
}
|
||||
$order_items_collection[ $order_item->order_id ][] = $order_item;
|
||||
return $order_items_collection;
|
||||
}
|
||||
);
|
||||
foreach ( $order_items_for_all_orders as $order_id => $items ) {
|
||||
wp_cache_set( 'order-items-' . $order_id, $items, 'orders' );
|
||||
}
|
||||
foreach ( $order_items as $item ) {
|
||||
wp_cache_set( 'item-' . $item->order_item_id, $item, 'order-items' );
|
||||
}
|
||||
$order_item_ids = wp_list_pluck( $order_items, 'order_item_id' );
|
||||
update_meta_cache( 'order_item', $order_item_ids );
|
||||
}
|
||||
|
||||
/**
|
||||
* Prime cache for raw meta data for orders in bulk. Difference between this and WP built-in metadata is that this method also fetches `meta_id` field which we use and cache it.
|
||||
*
|
||||
* @param array $order_ids Order Ids to prime cache for.
|
||||
* @param array $query_vars Query vars for the query.
|
||||
*/
|
||||
private function prime_raw_meta_cache_for_orders( $order_ids, $query_vars ) {
|
||||
global $wpdb;
|
||||
|
||||
if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] ) {
|
||||
if ( is_array( $query_vars['fields'] ) && ! in_array( 'meta_data', $query_vars['fields'] ) ) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$cache_keys_mapping = array();
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
$cache_keys_mapping[ $order_id ] = WC_Order::generate_meta_cache_key( $order_id, 'orders' );
|
||||
}
|
||||
$cache_values = wc_cache_get_multiple( array_values( $cache_keys_mapping ), 'orders' );
|
||||
$non_cached_ids = array();
|
||||
foreach ( $order_ids as $order_id ) {
|
||||
if ( false === $cache_values[ $cache_keys_mapping[ $order_id ] ] ) {
|
||||
$non_cached_ids[] = $order_id;
|
||||
}
|
||||
}
|
||||
if ( empty( $non_cached_ids ) ) {
|
||||
return;
|
||||
}
|
||||
$order_ids = esc_sql( $non_cached_ids );
|
||||
$order_ids_in = "'" . implode( "', '", $order_ids ) . "'";
|
||||
$raw_meta_data_array = $wpdb->get_results(
|
||||
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT post_id as object_id, meta_id, meta_key, meta_value
|
||||
FROM {$wpdb->postmeta}
|
||||
WHERE post_id IN ( $order_ids_in )
|
||||
ORDER BY post_id"
|
||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
$raw_meta_data_collection = array_reduce(
|
||||
$raw_meta_data_array,
|
||||
function ( $collection, $raw_meta_data ) {
|
||||
if ( ! isset( $collection[ $raw_meta_data->object_id ] ) ) {
|
||||
$collection[ $raw_meta_data->object_id ] = array();
|
||||
}
|
||||
$collection[ $raw_meta_data->object_id ][] = $raw_meta_data;
|
||||
return $collection;
|
||||
},
|
||||
array()
|
||||
);
|
||||
WC_Order::prime_raw_meta_data_cache( $raw_meta_data_collection, 'orders' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the order type of a given item which belongs to WC_Order.
|
||||
*
|
||||
|
|
|
@ -47,12 +47,15 @@ class WC_Order_Refund_Data_Store_CPT extends Abstract_WC_Order_Data_Store_CPT im
|
|||
*/
|
||||
public function delete( &$order, $args = array() ) {
|
||||
$id = $order->get_id();
|
||||
$parent_order_id = $order->get_parent_id();
|
||||
$refund_cache_key = WC_Cache_Helper::get_cache_prefix( 'orders' ) . 'refunds' . $parent_order_id;
|
||||
|
||||
if ( ! $id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_delete_post( $id );
|
||||
wp_cache_delete( $refund_cache_key, 'orders' );
|
||||
$order->set_id( 0 );
|
||||
do_action( 'woocommerce_delete_order_refund', $id );
|
||||
}
|
||||
|
|
|
@ -170,8 +170,8 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
|
|||
array(
|
||||
'name' => $post_object->post_title,
|
||||
'slug' => $post_object->post_name,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
'description' => $post_object->post_content,
|
||||
'short_description' => $post_object->post_excerpt,
|
||||
|
|
|
@ -62,8 +62,8 @@ class WC_Product_Variation_Data_Store_CPT extends WC_Product_Data_Store_CPT impl
|
|||
array(
|
||||
'name' => $post_object->post_title,
|
||||
'slug' => $post_object->post_name,
|
||||
'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null,
|
||||
'date_modified' => 0 < $post_object->post_modified_gmt ? wc_string_to_timestamp( $post_object->post_modified_gmt ) : null,
|
||||
'date_created' => $this->string_to_timestamp( $post_object->post_date_gmt ),
|
||||
'date_modified' => $this->string_to_timestamp( $post_object->post_modified_gmt ),
|
||||
'status' => $post_object->post_status,
|
||||
'menu_order' => $post_object->menu_order,
|
||||
'reviews_allowed' => 'open' === $post_object->comment_status,
|
||||
|
|
|
@ -1048,11 +1048,14 @@ class WC_Email extends WC_Settings_API {
|
|||
var view = '" . esc_js( __( 'View template', 'woocommerce' ) ) . "';
|
||||
var hide = '" . esc_js( __( 'Hide template', 'woocommerce' ) ) . "';
|
||||
|
||||
jQuery( 'a.toggle_editor' ).text( view ).toggle( function() {
|
||||
jQuery( this ).text( hide ).closest(' .template' ).find( '.editor' ).slideToggle();
|
||||
return false;
|
||||
}, function() {
|
||||
jQuery( this ).text( view ).closest( '.template' ).find( '.editor' ).slideToggle();
|
||||
jQuery( 'a.toggle_editor' ).text( view ).click( function() {
|
||||
var label = hide;
|
||||
|
||||
if ( jQuery( this ).closest(' .template' ).find( '.editor' ).is(':visible') ) {
|
||||
var label = view;
|
||||
}
|
||||
|
||||
jQuery( this ).text( label ).closest(' .template' ).find( '.editor' ).slideToggle();
|
||||
return false;
|
||||
} );
|
||||
|
||||
|
|
|
@ -58,7 +58,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
*/
|
||||
public function register_routes() {
|
||||
register_rest_route(
|
||||
$this->namespace, '/' . $this->rest_base, array(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
'args' => array(
|
||||
'order_id' => array(
|
||||
'description' => __( 'The order ID.', 'woocommerce' ),
|
||||
|
@ -82,7 +84,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
);
|
||||
|
||||
register_rest_route(
|
||||
$this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)', array(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/(?P<id>[\d]+)',
|
||||
array(
|
||||
'args' => array(
|
||||
'order_id' => array(
|
||||
'description' => __( 'The order ID.', 'woocommerce' ),
|
||||
|
@ -140,7 +144,7 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
$data = $object->get_data();
|
||||
$format_decimal = array( 'amount' );
|
||||
$format_date = array( 'date_created' );
|
||||
$format_line_items = array( 'line_items' );
|
||||
$format_line_items = array( 'line_items', 'shipping_lines', 'tax_lines', 'fee_lines' );
|
||||
|
||||
// Format decimal values.
|
||||
foreach ( $format_decimal as $key ) {
|
||||
|
@ -169,6 +173,9 @@ class WC_REST_Order_Refunds_V2_Controller extends WC_REST_Orders_V2_Controller {
|
|||
'refunded_payment' => $data['refunded_payment'],
|
||||
'meta_data' => $data['meta_data'],
|
||||
'line_items' => $data['line_items'],
|
||||
'shipping_lines' => $data['shipping_lines'],
|
||||
'tax_lines' => $data['tax_lines'],
|
||||
'fee_lines' => $data['fee_lines'],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -283,6 +283,7 @@ abstract class WC_REST_CRUD_Controller extends WC_REST_Posts_Controller {
|
|||
$args['post_parent__in'] = $request['parent'];
|
||||
$args['post_parent__not_in'] = $request['parent_exclude'];
|
||||
$args['s'] = $request['search'];
|
||||
$args['fields'] = $this->get_fields_for_response( $request );
|
||||
|
||||
if ( 'date' === $args['orderby'] ) {
|
||||
$args['orderby'] = 'date ID';
|
||||
|
|
|
@ -2501,3 +2501,23 @@ function wc_is_running_from_async_action_scheduler() {
|
|||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return isset( $_REQUEST['action'] ) && 'as_async_request_queue_runner' === $_REQUEST['action'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for wp_cache_get_multiple for WP versions before 5.5.
|
||||
*
|
||||
* @param array $keys Array of keys to get from group.
|
||||
* @param string $group Optional. Where the cache contents are grouped. Default empty.
|
||||
* @param bool $force Optional. Whether to force an update of the local cache from the persistent
|
||||
* cache. Default false.
|
||||
* @return array|bool Array of values.
|
||||
*/
|
||||
function wc_cache_get_multiple( $keys, $group = '', $force = false ) {
|
||||
if ( function_exists( 'wp_cache_get_multiple' ) ) {
|
||||
return wp_cache_get_multiple( $keys, $group, $force );
|
||||
}
|
||||
$values = array();
|
||||
foreach ( $keys as $key ) {
|
||||
$values[ $key ] = wp_cache_get( $key, $group, $force );
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ function wc_get_orders( $args ) {
|
|||
*
|
||||
* @since 2.2
|
||||
*
|
||||
* @param mixed $the_order Post object or post ID of the order.
|
||||
* @param mixed $the_order Post object or post ID of the order.
|
||||
*
|
||||
* @return bool|WC_Order|WC_Order_Refund
|
||||
*/
|
||||
|
|
|
@ -2491,7 +2491,7 @@ if ( ! function_exists( 'woocommerce_output_product_categories' ) ) {
|
|||
|
||||
foreach ( $product_categories as $category ) {
|
||||
wc_get_template(
|
||||
'content-product_cat.php',
|
||||
'content-product-cat.php',
|
||||
array(
|
||||
'category' => $category,
|
||||
)
|
||||
|
|
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "woocommerce",
|
||||
"title": "WooCommerce",
|
||||
"version": "4.8.0",
|
||||
"version": "4.9.0",
|
||||
"homepage": "https://woocommerce.com/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -13,7 +13,7 @@
|
|||
"wp_org_slug": "woocommerce"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "lerna bootstrap",
|
||||
"install": "lerna bootstrap --hoist",
|
||||
"build": "./bin/build-zip.sh",
|
||||
"build:core": "grunt && npm run makepot",
|
||||
"build:dev": "npm run build:core && npm run build:packages",
|
||||
|
@ -34,11 +34,11 @@
|
|||
"git:update-hooks": "rm -r .git/hooks && mkdir -p .git/hooks && node ./node_modules/husky/husky.js install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "7.12.0",
|
||||
"@babel/core": "7.12.0",
|
||||
"@babel/polyfill": "7.11.5",
|
||||
"@babel/preset-env": "7.12.0",
|
||||
"@babel/register": "7.12.0",
|
||||
"@babel/cli": "7.12.1",
|
||||
"@babel/core": "7.12.3",
|
||||
"@babel/polyfill": "7.12.1",
|
||||
"@babel/preset-env": "7.12.1",
|
||||
"@babel/register": "7.12.1",
|
||||
"@typescript-eslint/eslint-plugin": "3.10.1",
|
||||
"@typescript-eslint/experimental-utils": "3.10.1",
|
||||
"@typescript-eslint/parser": "3.10.1",
|
||||
|
@ -48,8 +48,8 @@
|
|||
"@woocommerce/e2e-utils": "file:tests/e2e/utils",
|
||||
"@wordpress/babel-plugin-import-jsx-pragma": "1.1.3",
|
||||
"@wordpress/babel-preset-default": "3.0.2",
|
||||
"@wordpress/e2e-test-utils": "4.6.0",
|
||||
"@wordpress/eslint-plugin": "7.1.0",
|
||||
"@wordpress/e2e-test-utils": "^4.6.0",
|
||||
"@wordpress/eslint-plugin": "7.3.0",
|
||||
"autoprefixer": "9.8.6",
|
||||
"babel-eslint": "10.1.0",
|
||||
"chai": "4.2.0",
|
||||
|
@ -77,17 +77,16 @@
|
|||
"gruntify-eslint": "5.0.0",
|
||||
"husky": "4.3.0",
|
||||
"istanbul": "1.0.0-alpha.2",
|
||||
"jest": "25.1.0",
|
||||
"jest": "^25.1.0",
|
||||
"lerna": "3.22.1",
|
||||
"lint-staged": "9.5.0",
|
||||
"mocha": "7.2.0",
|
||||
"node-sass": "4.13.1",
|
||||
"node-sass": "4.14.1",
|
||||
"prettier": "npm:wp-prettier@2.0.5",
|
||||
"puppeteer": "^2.1.1",
|
||||
"stylelint": "12.0.1",
|
||||
"stylelint-config-wordpress": "16.0.0",
|
||||
"typescript": "3.9.7",
|
||||
"webpack": "4.44.1",
|
||||
"webpack": "4.44.2",
|
||||
"webpack-cli": "3.3.12",
|
||||
"wp-textdomain": "1.0.1"
|
||||
},
|
||||
|
|
|
@ -4,7 +4,7 @@ Tags: e-commerce, store, sales, sell, woo, shop, cart, checkout, downloadable, d
|
|||
Requires at least: 5.3
|
||||
Tested up to: 5.5
|
||||
Requires PHP: 7.0
|
||||
Stable tag: 4.6.1
|
||||
Stable tag: 4.6.2
|
||||
License: GPLv3
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
|
@ -160,7 +160,7 @@ WooCommerce comes with some sample data you can use to see how products look; im
|
|||
|
||||
== Changelog ==
|
||||
|
||||
= 4.8.0 - 2020-12-xx =
|
||||
= 4.9.0 - 2021-01-xx =
|
||||
|
||||
[See changelog for all versions](https://raw.githubusercontent.com/woocommerce/woocommerce/master/changelog.txt).
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
|
|||
),
|
||||
$product
|
||||
);
|
||||
$show_add_to_cart_button = false;
|
||||
|
||||
do_action( 'woocommerce_grouped_product_list_before', $grouped_product_columns, $quantites_required, $product );
|
||||
|
||||
|
@ -45,6 +46,10 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
|
|||
$post = $post_object; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
setup_postdata( $post );
|
||||
|
||||
if ( $grouped_product_child->is_in_stock() ) {
|
||||
$show_add_to_cart_button = true;
|
||||
}
|
||||
|
||||
echo '<tr id="product-' . esc_attr( $grouped_product_child->get_id() ) . '" class="woocommerce-grouped-product-list-item ' . esc_attr( implode( ' ', wc_get_product_class( '', $grouped_product_child ) ) ) . '">';
|
||||
|
||||
// Output columns for each product.
|
||||
|
@ -107,7 +112,7 @@ do_action( 'woocommerce_before_add_to_cart_form' ); ?>
|
|||
|
||||
<input type="hidden" name="add-to-cart" value="<?php echo esc_attr( $product->get_id() ); ?>" />
|
||||
|
||||
<?php if ( $quantites_required ) : ?>
|
||||
<?php if ( $quantites_required && $show_add_to_cart_button ) : ?>
|
||||
|
||||
<?php do_action( 'woocommerce_before_add_to_cart_button' ); ?>
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ if [[ ${RUN_PHPCS} == 1 ]] || [[ ${RUN_E2E} == 1 ]]; then
|
|||
fi
|
||||
|
||||
if [[ ${RUN_CODE_COVERAGE} == 1 ]]; then
|
||||
phpdbg -qrr $HOME/.composer/vendor/bin/phpunit -d memory_limit=-1 -c phpunit.xml --coverage-clover=coverage.clover --exclude-group=timeout $@
|
||||
phpdbg -qrr ./vendor/bin/phpunit -d memory_limit=-1 -c phpunit.xml --coverage-clover=coverage.clover --exclude-group=timeout $@
|
||||
else
|
||||
$HOME/.composer/vendor/bin/phpunit -c phpunit.xml $@
|
||||
vendor/bin/phpunit -c phpunit.xml $@
|
||||
fi
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# Unreleased
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
- The `HTTPClientFactory` API was changed to make it easier to configure instances with
|
||||
|
||||
## Added
|
||||
|
||||
- `HTTPClientFactory` methods `withIndexPermalinks()` and `withoutIndexPermalinks()` to enable/disable API pretty permalinks
|
||||
- Expanded properties of `AbstractProduct` model type
|
||||
- Added `list`, `read`, `update`, and `delete` operations for `SimpleProduct` repositories
|
||||
|
||||
## Changes
|
||||
|
||||
- Added a tranformation layer between API responses and internal models
|
||||
|
||||
# 0.1.0
|
||||
|
||||
- Initial/beta release
|
|
@ -24,24 +24,14 @@ The simplest way to use the client is directly:
|
|||
import { HTTPClientFactory } from '@woocommerce/api';
|
||||
|
||||
// You can create an API client using the client factory with pre-configured middleware for convenience.
|
||||
let httpClient = HTTPClientFactory.withBasicAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The username for your WordPress user.
|
||||
'username',
|
||||
// The password for your WordPress user.
|
||||
'password',
|
||||
);
|
||||
let client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
// You can also create an API client configured for requests using OAuth.
|
||||
httpClient = HTTPClientFactory.withOAuth(
|
||||
// The base URL of your REST API.
|
||||
'https://example.com/wp-json/',
|
||||
// The OAuth API Key's consumer secret.
|
||||
'consumer_secret',
|
||||
// The OAuth API Key's consumer password.
|
||||
'consumer_pasword',
|
||||
);
|
||||
client = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withOAuth( 'consumer_secret', 'consumer_password' )
|
||||
.create();
|
||||
|
||||
// You can then use the client to make API requests.
|
||||
httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
||||
|
@ -54,6 +44,7 @@ httpClient.get( '/wc/v3/products' ).then( ( response ) => {
|
|||
}, ( error ) => {
|
||||
// Handle errors that may have come up.
|
||||
} );
|
||||
|
||||
```
|
||||
|
||||
### Repositories
|
||||
|
@ -66,7 +57,9 @@ import { SimpleProduct } from '@woocommerce/api';
|
|||
|
||||
// Prepare the HTTP client that will be consumed by the repository.
|
||||
// This is necessary so that it can make requests to the REST API.
|
||||
const httpClient = HTTPClientFactory.withBasicAuth( 'https://example.com/wp-json/','username','password' );
|
||||
const httpClient = HTTPClientFactory.build( 'https://example.com' )
|
||||
.withBasicAuth( 'username', 'password' )
|
||||
.create();
|
||||
|
||||
const repository = SimpleProduct.restRepository( httpClient );
|
||||
|
||||
|
|
|
@ -21,7 +21,8 @@
|
|||
"!*.tsbuildinfo",
|
||||
"!/dist/**/__tests__/",
|
||||
"!/dist/**/__mocks__/",
|
||||
"!/dist/**/__snapshops__/"
|
||||
"!/dist/**/__snapshops__/",
|
||||
"!/dist/**/__test_data__/"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
|
@ -42,7 +43,7 @@
|
|||
"@types/jest": "25.2.1",
|
||||
"@types/moxios": "^0.4.9",
|
||||
"@types/node": "13.13.5",
|
||||
"jest": "25.5.4",
|
||||
"jest": "^25.1.0",
|
||||
"jest-mock-extended": "^1.0.10",
|
||||
"moxios": "0.4.0",
|
||||
"ts-jest": "25.5.0",
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Model } from '../models/model';
|
||||
|
||||
/**
|
||||
* A dummy model that can be used in test files.
|
||||
*/
|
||||
export class DummyModel extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
|
@ -12,15 +12,8 @@ import {
|
|||
UpdatesChildModels,
|
||||
UpdatesModels,
|
||||
} from '../model-repository';
|
||||
import { DummyModel } from '../../__test_data__/dummy-model';
|
||||
|
||||
class DummyModel extends Model {
|
||||
public name: string = '';
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
||||
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
|
||||
|
||||
class DummyChildModel extends Model {
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { ModelTransformation, ModelTransformer } from '../model-transformer';
|
||||
import { DummyModel } from '../../__test_data__/dummy-model';
|
||||
|
||||
class DummyTransformation implements ModelTransformation {
|
||||
public readonly fromModelOrder: number;
|
||||
|
||||
private readonly fn: ( ( p: any ) => any ) | null;
|
||||
|
||||
public constructor( order: number, fn: ( ( p: any ) => any ) | null ) {
|
||||
this.fromModelOrder = order;
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
public fromModel( properties: any ): any {
|
||||
if ( ! this.fn ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.fn( properties );
|
||||
}
|
||||
|
||||
public toModel( properties: any ): any {
|
||||
if ( ! this.fn ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.fn( properties );
|
||||
}
|
||||
}
|
||||
|
||||
describe( 'ModelTransformer', () => {
|
||||
it( 'should order transformers correctly', () => {
|
||||
const fn1 = jest.fn();
|
||||
fn1.mockReturnValue( { name: 'fn1' } );
|
||||
const fn2 = jest.fn();
|
||||
fn2.mockReturnValue( { name: 'fn2' } );
|
||||
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
// Ensure the orders are backwards so sorting is tested.
|
||||
new DummyTransformation( 1, fn2 ),
|
||||
new DummyTransformation( 0, fn1 ),
|
||||
],
|
||||
);
|
||||
|
||||
let transformed = transformer.fromModel( new DummyModel( { name: 'fn0' } ) );
|
||||
|
||||
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn0' } );
|
||||
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn1' } );
|
||||
expect( transformed ).toMatchObject( { name: 'fn2' } );
|
||||
|
||||
// Reset and make sure "toModel" happens in reverse order.
|
||||
fn1.mockClear();
|
||||
fn2.mockClear();
|
||||
|
||||
transformed = transformer.toModel( DummyModel, { name: 'fn3' } );
|
||||
|
||||
expect( fn2 ).toHaveBeenCalledWith( { name: 'fn3' } );
|
||||
expect( fn1 ).toHaveBeenCalledWith( { name: 'fn2' } );
|
||||
expect( transformed ).toMatchObject( { name: 'fn1' } );
|
||||
} );
|
||||
|
||||
it( 'should transform to model', () => {
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
new DummyTransformation(
|
||||
0,
|
||||
( p: any ) => {
|
||||
p.name = 'Transformed-' + p.name;
|
||||
return p;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const model = transformer.toModel( DummyModel, { name: 'Test' } );
|
||||
|
||||
expect( model ).toBeInstanceOf( DummyModel );
|
||||
expect( model.name ).toEqual( 'Transformed-Test' );
|
||||
} );
|
||||
|
||||
it( 'should transform from model', () => {
|
||||
const transformer = new ModelTransformer< DummyModel >(
|
||||
[
|
||||
new DummyTransformation(
|
||||
0,
|
||||
( p: any ) => {
|
||||
p.name = 'Transformed-' + p.name;
|
||||
return p;
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const transformed = transformer.fromModel( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
expect( transformed ).not.toBeInstanceOf( DummyModel );
|
||||
expect( transformed.name ).toEqual( 'Transformed-Test' );
|
||||
} );
|
||||
} );
|
|
@ -31,13 +31,14 @@ export interface ModelRepositoryParams<
|
|||
/**
|
||||
* These helpers will extract information about a model from its repository params to be used in the repository.
|
||||
*/
|
||||
type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
|
||||
type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
|
||||
export type ModelClass< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer X > ] ? X : never;
|
||||
export type ParentID< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, infer X > ] ? X : never;
|
||||
export type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
|
||||
type ListParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< any, any, infer X > ] ? X : never;
|
||||
type PickUpdateParams<T, K extends keyof T> = { [P in K]?: T[P]; };
|
||||
type UpdateParams< T extends ModelRepositoryParams > = [ T ] extends [ ModelRepositoryParams< infer C, any, any, infer X > ] ?
|
||||
( [ X ] extends [ keyof C ] ? Pick< C, X > : never ) :
|
||||
( [ X ] extends [ keyof C ] ? PickUpdateParams< C, X > : never ) :
|
||||
never;
|
||||
type HasParent< T extends ModelRepositoryParams, P, C > = [ ParentID< T > ] extends [ never ] ? C : P;
|
||||
|
||||
/**
|
||||
* A callback for listing models using a data source.
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import { Model } from '../models/model';
|
||||
import { ModelConstructor } from '../models/shared-types';
|
||||
|
||||
/**
|
||||
* An interface for an object that can perform transformations both to and from a representation
|
||||
* and return the input data after performing the desired transformation.
|
||||
*
|
||||
* @interface ModelTransformation
|
||||
*/
|
||||
export interface ModelTransformation {
|
||||
/**
|
||||
* The order of execution for the transformer.
|
||||
* - For "fromModel" higher numbers execute later.
|
||||
* - For "toModel" the order is reversed.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
readonly fromModelOrder: number;
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
fromModel( properties: any ): any;
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
toModel( properties: any ): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* An enum for defining the "toModel" transformation order values.
|
||||
*/
|
||||
export enum TransformationOrder {
|
||||
First = 0,
|
||||
Normal = 500000,
|
||||
Last = 1000000,
|
||||
|
||||
/**
|
||||
* A special value reserved for transformations that MUST come after all orders due to
|
||||
* the way that they destroy the property keys or values.
|
||||
*/
|
||||
VeryLast = 2000000
|
||||
}
|
||||
|
||||
/**
|
||||
* A class for transforming models to/from a generic representation.
|
||||
*/
|
||||
export class ModelTransformer< T extends Model > {
|
||||
/**
|
||||
* An array of transformations to use when converting data to/from models.
|
||||
*
|
||||
* @type {Array.<ModelTransformation>}
|
||||
* @private
|
||||
*/
|
||||
private transformations: readonly ModelTransformation[];
|
||||
|
||||
/**
|
||||
* Creates a new model transformer instance.
|
||||
*
|
||||
* @param {Array.<ModelTransformation>} transformations The transformations to use.
|
||||
*/
|
||||
public constructor( transformations: ModelTransformation[] ) {
|
||||
// Ensure that the transformations are sorted by priority.
|
||||
transformations.sort( ( a, b ) => ( a.fromModelOrder > b.fromModelOrder ) ? 1 : -1 );
|
||||
|
||||
this.transformations = transformations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the input model and runs all of the transformations on it before returning the data.
|
||||
*
|
||||
* @param {Partial.<T>} model The model to transform.
|
||||
* @return {*} The transformed data.
|
||||
* @template T
|
||||
*/
|
||||
public fromModel( model: Partial< T > ): any {
|
||||
// Convert the model class to raw properties so that the transformations can be simple.
|
||||
const raw = Object.assign( {}, model );
|
||||
|
||||
return this.transformations.reduce(
|
||||
( properties: any, transformer: ModelTransformation ) => {
|
||||
return transformer.fromModel( properties );
|
||||
},
|
||||
raw,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the input data and runs all of the transformations on it before returning the created model.
|
||||
*
|
||||
* @param {Function.<T>} modelClass The model class we're trying to create.
|
||||
* @param {*} data The data we're transforming.
|
||||
* @return {T} The transformed model.
|
||||
* @template T
|
||||
*/
|
||||
public toModel( modelClass: ModelConstructor< T >, data: any ): T {
|
||||
const transformed: any = this.transformations.reduceRight(
|
||||
( properties: any, transformer: ModelTransformation ) => {
|
||||
return transformer.toModel( properties );
|
||||
},
|
||||
data,
|
||||
);
|
||||
|
||||
return new modelClass( transformed );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { AddPropertyTransformation } from '../add-property-transformation';
|
||||
|
||||
describe( 'AddPropertyTransformation', () => {
|
||||
let transformation: AddPropertyTransformation;
|
||||
|
||||
beforeEach( () => {
|
||||
transformation = new AddPropertyTransformation(
|
||||
{ toProperty: 'Test' },
|
||||
{ fromProperty: 'Test' },
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should add property when missing', () => {
|
||||
let transformed = transformation.toModel( { id: 1, name: 'Test' } );
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
toProperty: 'Test',
|
||||
},
|
||||
);
|
||||
|
||||
transformed = transformation.fromModel( { id: 1, name: 'Test' } );
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
fromProperty: 'Test',
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should not add property when present', () => {
|
||||
let transformed = transformation.toModel( { id: 1, name: 'Test', toProperty: 'Existing' } );
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
toProperty: 'Existing',
|
||||
},
|
||||
);
|
||||
|
||||
transformed = transformation.fromModel( { id: 1, name: 'Test', fromProperty: 'Existing' } );
|
||||
|
||||
expect( transformed ).toMatchObject(
|
||||
{
|
||||
id: 1,
|
||||
name: 'Test',
|
||||
fromProperty: 'Existing',
|
||||
},
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,26 @@
|
|||
import { CustomTransformation } from '../custom-transformation';
|
||||
|
||||
describe( 'CustomTransformation', () => {
|
||||
it( 'should do nothing without hooks', () => {
|
||||
const transformation = new CustomTransformation( 0, null, null );
|
||||
|
||||
const expected = { test: 'Test' };
|
||||
|
||||
expect( transformation.toModel( expected ) ).toMatchObject( expected );
|
||||
expect( transformation.fromModel( expected ) ).toMatchObject( expected );
|
||||
} );
|
||||
|
||||
it( 'should execute hooks', () => {
|
||||
const toHook = jest.fn();
|
||||
toHook.mockReturnValue( { toModel: 'Test' } );
|
||||
const fromHook = jest.fn();
|
||||
fromHook.mockReturnValue( { fromModel: 'Test' } );
|
||||
|
||||
const transformation = new CustomTransformation( 0, toHook, fromHook );
|
||||
|
||||
expect( transformation.toModel( { test: 'Test' } ) ).toMatchObject( { toModel: 'Test' } );
|
||||
expect( toHook ).toHaveBeenCalledWith( { test: 'Test' } );
|
||||
expect( transformation.fromModel( { test: 'Test' } ) ).toMatchObject( { fromModel: 'Test' } );
|
||||
expect( fromHook ).toHaveBeenCalledWith( { test: 'Test' } );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,31 @@
|
|||
import { IgnorePropertyTransformation } from '../ignore-property-transformation';
|
||||
|
||||
describe( 'IgnorePropertyTransformation', () => {
|
||||
let transformation: IgnorePropertyTransformation;
|
||||
|
||||
beforeEach( () => {
|
||||
transformation = new IgnorePropertyTransformation( [ 'skip' ] );
|
||||
} );
|
||||
|
||||
it( 'should remove ignored properties', () => {
|
||||
let transformed = transformation.fromModel(
|
||||
{
|
||||
test: 'Test',
|
||||
skip: 'Test',
|
||||
},
|
||||
);
|
||||
|
||||
expect( transformed ).toHaveProperty( 'test', 'Test' );
|
||||
expect( transformed ).not.toHaveProperty( 'skip' );
|
||||
|
||||
transformed = transformation.toModel(
|
||||
{
|
||||
test: 'Test',
|
||||
skip: 'Test',
|
||||
},
|
||||
);
|
||||
|
||||
expect( transformed ).toHaveProperty( 'test', 'Test' );
|
||||
expect( transformed ).not.toHaveProperty( 'skip' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,28 @@
|
|||
import { KeyChangeTransformation } from '../key-change-transformation';
|
||||
import { DummyModel } from '../../../__test_data__/dummy-model';
|
||||
|
||||
describe( 'KeyChangeTransformation', () => {
|
||||
let transformation: KeyChangeTransformation< DummyModel >;
|
||||
|
||||
beforeEach( () => {
|
||||
transformation = new KeyChangeTransformation< DummyModel >(
|
||||
{
|
||||
name: 'new-name',
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should transform to model', () => {
|
||||
const transformed = transformation.toModel( { 'new-name': 'Test Name' } );
|
||||
|
||||
expect( transformed ).toHaveProperty( 'name', 'Test Name' );
|
||||
expect( transformed ).not.toHaveProperty( 'new-name' );
|
||||
} );
|
||||
|
||||
it( 'should transform from model', () => {
|
||||
const transformed = transformation.fromModel( { name: 'Test Name' } );
|
||||
|
||||
expect( transformed ).toHaveProperty( 'new-name', 'Test Name' );
|
||||
expect( transformed ).not.toHaveProperty( 'name' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,52 @@
|
|||
import { ModelTransformerTransformation } from '../model-transformer-transformation';
|
||||
import { ModelTransformer } from '../../model-transformer';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { DummyModel } from '../../../__test_data__/dummy-model';
|
||||
|
||||
describe( 'ModelTransformerTransformation', () => {
|
||||
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
|
||||
let transformation: ModelTransformerTransformation< any >;
|
||||
|
||||
beforeEach( () => {
|
||||
mockTransformer = mock< ModelTransformer< any > >();
|
||||
transformation = new ModelTransformerTransformation< DummyModel >(
|
||||
'test',
|
||||
DummyModel,
|
||||
mockTransformer,
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should execute child transformer', () => {
|
||||
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
|
||||
|
||||
let transformed = transformation.toModel( { test: 'Test' } );
|
||||
|
||||
expect( transformed ).toMatchObject( { test: { toModel: 'Test' } } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
|
||||
|
||||
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
|
||||
|
||||
transformed = transformation.fromModel( { test: 'Test' } );
|
||||
|
||||
expect( transformed ).toMatchObject( { test: { fromModel: 'Test' } } );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
|
||||
} );
|
||||
|
||||
it( 'should execute child transformer on array', () => {
|
||||
mockTransformer.toModel.mockReturnValue( { toModel: 'Test' } );
|
||||
|
||||
let transformed = transformation.toModel( { test: [ 'Test', 'Test2' ] } );
|
||||
|
||||
expect( transformed ).toMatchObject( { test: [ { toModel: 'Test' }, { toModel: 'Test' } ] } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test' );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, 'Test2' );
|
||||
|
||||
mockTransformer.fromModel.mockReturnValue( { fromModel: 'Test' } );
|
||||
|
||||
transformed = transformation.fromModel( { test: [ 'Test', 'Test2' ] } );
|
||||
|
||||
expect( transformed ).toMatchObject( { test: [ { fromModel: 'Test' }, { fromModel: 'Test' } ] } );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test' );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( 'Test2' );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,108 @@
|
|||
import { PropertyType, PropertyTypeTransformation } from '../property-type-transformation';
|
||||
|
||||
describe( 'PropertyTypeTransformation', () => {
|
||||
let transformation: PropertyTypeTransformation;
|
||||
|
||||
beforeEach( () => {
|
||||
transformation = new PropertyTypeTransformation(
|
||||
{
|
||||
string: PropertyType.String,
|
||||
integer: PropertyType.Integer,
|
||||
float: PropertyType.Float,
|
||||
boolean: PropertyType.Boolean,
|
||||
date: PropertyType.Date,
|
||||
callback: ( value: string ) => 'Transformed-' + value,
|
||||
},
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should convert strings', () => {
|
||||
let transformed = transformation.toModel( { string: 'Test' } );
|
||||
|
||||
expect( transformed.string ).toStrictEqual( 'Test' );
|
||||
|
||||
transformed = transformation.fromModel( { string: 'Test' } );
|
||||
|
||||
expect( transformed.string ).toStrictEqual( 'Test' );
|
||||
} );
|
||||
|
||||
it( 'should convert integers', () => {
|
||||
let transformed = transformation.toModel( { integer: '100' } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( 100 );
|
||||
|
||||
transformed = transformation.fromModel( { integer: 100 } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( '100' );
|
||||
} );
|
||||
|
||||
it( 'should convert floats', () => {
|
||||
let transformed = transformation.toModel( { float: '2.5' } );
|
||||
|
||||
expect( transformed.float ).toStrictEqual( 2.5 );
|
||||
|
||||
transformed = transformation.fromModel( { float: 2.5 } );
|
||||
|
||||
expect( transformed.float ).toStrictEqual( '2.5' );
|
||||
} );
|
||||
|
||||
it( 'should convert booleans', () => {
|
||||
let transformed = transformation.toModel( { boolean: 'true' } );
|
||||
|
||||
expect( transformed.boolean ).toStrictEqual( true );
|
||||
|
||||
transformed = transformation.fromModel( { boolean: false } );
|
||||
|
||||
expect( transformed.boolean ).toStrictEqual( 'false' );
|
||||
} );
|
||||
|
||||
it( 'should convert dates', () => {
|
||||
let transformed = transformation.toModel( { date: '2020-11-06T03:11:41.000Z' } );
|
||||
|
||||
expect( transformed.date ).toStrictEqual( new Date( '2020-11-06T03:11:41.000Z' ) );
|
||||
|
||||
transformed = transformation.fromModel( { date: new Date( '2020-11-06T03:11:41.000Z' ) } );
|
||||
|
||||
expect( transformed.date ).toStrictEqual( '2020-11-06T03:11:41.000Z' );
|
||||
} );
|
||||
|
||||
it( 'should use conversion callbacks', () => {
|
||||
let transformed = transformation.toModel( { callback: 'Test' } );
|
||||
|
||||
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
|
||||
|
||||
transformed = transformation.fromModel( { callback: 'Test' } );
|
||||
|
||||
expect( transformed.callback ).toStrictEqual( 'Transformed-Test' );
|
||||
} );
|
||||
|
||||
it( 'should convert arrays', () => {
|
||||
let transformed = transformation.toModel( { integer: [ '100', '200', '300' ] } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( [ 100, 200, 300 ] );
|
||||
|
||||
transformed = transformation.fromModel( { integer: [ 100, 200, 300 ] } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( [ '100', '200', '300' ] );
|
||||
} );
|
||||
|
||||
it( 'should do nothing without property', () => {
|
||||
let transformed = transformation.toModel( { name: 'Test' } );
|
||||
|
||||
expect( transformed.name ).toStrictEqual( 'Test' );
|
||||
|
||||
transformed = transformation.fromModel( { name: 'Test' } );
|
||||
|
||||
expect( transformed.name ).toStrictEqual( 'Test' );
|
||||
} );
|
||||
|
||||
it( 'should preserve null', () => {
|
||||
let transformed = transformation.toModel( { integer: null } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( null );
|
||||
|
||||
transformed = transformation.fromModel( { integer: null } );
|
||||
|
||||
expect( transformed.integer ).toStrictEqual( null );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,76 @@
|
|||
import { ModelTransformation, TransformationOrder } from '../model-transformer';
|
||||
|
||||
/**
|
||||
* @typedef AdditionalProperties
|
||||
* @alias Object.<string,string>
|
||||
*/
|
||||
type AdditionalProperties = { [ key: string ]: any };
|
||||
|
||||
/**
|
||||
* A model transformation that adds a property with
|
||||
* a default value if it is not already set.
|
||||
*/
|
||||
export class AddPropertyTransformation implements ModelTransformation {
|
||||
public readonly fromModelOrder = TransformationOrder.Normal;
|
||||
|
||||
/**
|
||||
*The additional properties to add when executing toModel.
|
||||
*
|
||||
* @type {AdditionalProperties}
|
||||
* @private
|
||||
*/
|
||||
private readonly toProperties: AdditionalProperties;
|
||||
|
||||
/**
|
||||
* The additional properties to add when executing fromModel.
|
||||
*
|
||||
* @type {AdditionalProperties}
|
||||
* @private
|
||||
*/
|
||||
private readonly fromProperties: AdditionalProperties;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {AdditionalProperties} toProperties The properties to add when executing toModel.
|
||||
* @param {AdditionalProperties} fromProperties The properties to add when executing fromModel.
|
||||
*/
|
||||
public constructor( toProperties: AdditionalProperties, fromProperties: AdditionalProperties ) {
|
||||
this.toProperties = toProperties;
|
||||
this.fromProperties = fromProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
for ( const key in this.fromProperties ) {
|
||||
if ( properties.hasOwnProperty( key ) ) {
|
||||
continue;
|
||||
}
|
||||
properties[ key ] = this.fromProperties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
for ( const key in this.toProperties ) {
|
||||
if ( properties.hasOwnProperty( key ) ) {
|
||||
continue;
|
||||
}
|
||||
properties[ key ] = this.toProperties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { ModelTransformation } from '../model-transformer';
|
||||
|
||||
/**
|
||||
* A callback for transforming model properties.
|
||||
*
|
||||
* @callback TransformationCallback
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
type TransformationCallback = ( properties: any ) => any;
|
||||
|
||||
/**
|
||||
* A model transformer for executing arbitrary callbacks on input properties.
|
||||
*/
|
||||
export class CustomTransformation implements ModelTransformation {
|
||||
public readonly fromModelOrder: number;
|
||||
|
||||
/**
|
||||
* The hook to run for toModel.
|
||||
*
|
||||
* @type {TransformationCallback|null}
|
||||
* @private
|
||||
*/
|
||||
private readonly toHook: TransformationCallback | null;
|
||||
|
||||
/**
|
||||
* The hook to run for fromModel.
|
||||
*
|
||||
* @type {TransformationCallback|null}
|
||||
* @private
|
||||
*/
|
||||
private readonly fromHook: TransformationCallback | null;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {number} order The order for the transformation.
|
||||
* @param {TransformationCallback|null} toHook The hook to run for toModel.
|
||||
* @param {TransformationCallback|null} fromHook The hook to run for fromModel.
|
||||
*/
|
||||
public constructor(
|
||||
order: number,
|
||||
toHook: TransformationCallback | null,
|
||||
fromHook: TransformationCallback | null,
|
||||
) {
|
||||
this.fromModelOrder = order;
|
||||
this.toHook = toHook;
|
||||
this.fromHook = fromHook;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
if ( ! this.fromHook ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.fromHook( properties );
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
if ( ! this.toHook ) {
|
||||
return properties;
|
||||
}
|
||||
|
||||
return this.toHook( properties );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { ModelTransformation, TransformationOrder } from '../model-transformer';
|
||||
|
||||
export class IgnorePropertyTransformation implements ModelTransformation {
|
||||
public readonly fromModelOrder = TransformationOrder.Normal;
|
||||
|
||||
/**
|
||||
* A list of properties that should be removed.
|
||||
*
|
||||
* @type {Array.<string>}
|
||||
* @private
|
||||
*/
|
||||
private readonly ignoreList: readonly string[];
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {Array.<string>} ignoreList The properties to ignore.
|
||||
*/
|
||||
public constructor( ignoreList: string[] ) {
|
||||
this.ignoreList = ignoreList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
for ( const key of this.ignoreList ) {
|
||||
delete properties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
for ( const key of this.ignoreList ) {
|
||||
delete properties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import { ModelTransformation, TransformationOrder } from '../model-transformer';
|
||||
import { Model } from '../../models/model';
|
||||
|
||||
/**
|
||||
* @typedef KeyChanges
|
||||
* @alias Object.<string,string>
|
||||
*/
|
||||
type KeyChanges< T extends Model > = { readonly [ key in keyof Partial< T > ]: string };
|
||||
|
||||
/**
|
||||
* A model transformation that can be used to change property keys between two formats.
|
||||
* This transformation has a very high priority so that it will be executed after all
|
||||
* other transformations to prevent the changed key from causing problems.
|
||||
*/
|
||||
export class KeyChangeTransformation< T extends Model > implements ModelTransformation {
|
||||
/**
|
||||
* Ensure that this transformation always happens at the very end since it changes the keys
|
||||
* in the transformed object.
|
||||
*/
|
||||
public readonly fromModelOrder = TransformationOrder.VeryLast + 1;
|
||||
|
||||
/**
|
||||
* The key change transformations that this object should perform.
|
||||
* This is structured with the model's property key as the key
|
||||
* of the object and the raw property key as the value.
|
||||
*
|
||||
* @type {KeyChanges}
|
||||
* @private
|
||||
*/
|
||||
private readonly changes: KeyChanges< T >;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {KeyChanges} changes The changes we want the transformation to make.
|
||||
*/
|
||||
public constructor( changes: KeyChanges< T > ) {
|
||||
this.changes = changes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
for ( const key in this.changes ) {
|
||||
const value = this.changes[ key ];
|
||||
|
||||
if ( ! properties.hasOwnProperty( key ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[ value ] = properties[ key ];
|
||||
delete properties[ key ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
for ( const key in this.changes ) {
|
||||
const value = this.changes[ key ];
|
||||
|
||||
if ( ! properties.hasOwnProperty( value ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[ key ] = properties[ value ];
|
||||
delete properties[ value ];
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../model-transformer';
|
||||
import { Model } from '../../models/model';
|
||||
import { ModelConstructor } from '../../models/shared-types';
|
||||
|
||||
/**
|
||||
* A model transformation that applies another transformer to a property.
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
export class ModelTransformerTransformation< T extends Model > implements ModelTransformation {
|
||||
public readonly fromModelOrder = TransformationOrder.Normal;
|
||||
|
||||
/**
|
||||
* The property that the transformation should be applied to.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
private readonly property: string;
|
||||
|
||||
/**
|
||||
* The model class we want to transform into.
|
||||
*
|
||||
* @type {Function.<T>}
|
||||
* @private
|
||||
* @template T
|
||||
*/
|
||||
private readonly modelClass: ModelConstructor< T >;
|
||||
|
||||
/**
|
||||
* The transformer that should be used.
|
||||
*
|
||||
* @type {ModelTransformer}
|
||||
* @private
|
||||
*/
|
||||
private readonly transformer: ModelTransformer< T >;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {string} property The property we want to apply the transformer to.
|
||||
* @param {ModelConstructor.<T>} modelClass The model to transform into.
|
||||
* @param {ModelTransformer} transformer The transformer we want to apply.
|
||||
* @template T
|
||||
*/
|
||||
public constructor( property: string, modelClass: ModelConstructor< T >, transformer: ModelTransformer< T > ) {
|
||||
this.property = property;
|
||||
this.modelClass = modelClass;
|
||||
this.transformer = transformer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
const val = properties[ this.property ];
|
||||
if ( val ) {
|
||||
if ( Array.isArray( val ) ) {
|
||||
properties[ this.property ] = val.map( ( v ) => this.transformer.fromModel( v ) );
|
||||
} else {
|
||||
properties[ this.property ] = this.transformer.fromModel( val );
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
const val = properties[ this.property ];
|
||||
if ( val ) {
|
||||
if ( Array.isArray( val ) ) {
|
||||
properties[ this.property ] = val.map( ( v ) => this.transformer.toModel( this.modelClass, v ) );
|
||||
} else {
|
||||
properties[ this.property ] = this.transformer.toModel( this.modelClass, val );
|
||||
}
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
import { ModelTransformation, TransformationOrder } from '../model-transformer';
|
||||
|
||||
/**
|
||||
* An enum defining all of the property types that we might want to transform.
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
export enum PropertyType {
|
||||
String,
|
||||
Integer,
|
||||
Float,
|
||||
Boolean,
|
||||
Date,
|
||||
}
|
||||
type PropertyTypeTypes = null | string | number | boolean | Date;
|
||||
|
||||
/**
|
||||
* A callback that can be used to transform property types.
|
||||
*
|
||||
* @callback PropertyTypeCallback
|
||||
* @param {*} value The value to transform.
|
||||
* @return {*} The transformed value.
|
||||
*/
|
||||
type PropertyTypeCallback = ( value: any ) => any;
|
||||
|
||||
/**
|
||||
* The types for all of a model's properties.
|
||||
*
|
||||
* @typedef PropertyTypes
|
||||
* @alias Object.<string,PropertyType>
|
||||
*/
|
||||
type PropertyTypes = { [ key: string ]: PropertyType | PropertyTypeCallback };
|
||||
|
||||
/**
|
||||
* A model transformer for converting property types between representation formats.
|
||||
*/
|
||||
export class PropertyTypeTransformation implements ModelTransformation {
|
||||
/**
|
||||
* We want the type transformation to take place after all of the others,
|
||||
* since they may be operating on internal data types.
|
||||
*/
|
||||
public readonly fromModelOrder = TransformationOrder.VeryLast;
|
||||
|
||||
/**
|
||||
* The property types we will want to transform.
|
||||
*
|
||||
* @type {PropertyTypes}
|
||||
* @private
|
||||
*/
|
||||
private readonly types: PropertyTypes;
|
||||
|
||||
/**
|
||||
* Creates a new transformation.
|
||||
*
|
||||
* @param {PropertyTypes} types The property types we want to transform.
|
||||
*/
|
||||
public constructor( types: PropertyTypes ) {
|
||||
this.types = types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from model properties to raw properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public fromModel( properties: any ): any {
|
||||
for ( const key in this.types ) {
|
||||
if ( ! properties.hasOwnProperty( key ) ) {
|
||||
continue;
|
||||
}
|
||||
const value = properties[ key ];
|
||||
|
||||
const type = this.types[ key ];
|
||||
if ( type instanceof Function ) {
|
||||
properties[ key ] = type( value );
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[ key ] = this.convertFrom( value, type );
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a transformation from raw properties to model properties.
|
||||
*
|
||||
* @param {*} properties The properties to transform.
|
||||
* @return {*} The transformed properties.
|
||||
*/
|
||||
public toModel( properties: any ): any {
|
||||
for ( const key in this.types ) {
|
||||
if ( ! properties.hasOwnProperty( key ) ) {
|
||||
continue;
|
||||
}
|
||||
const value = properties[ key ];
|
||||
|
||||
const type = this.types[ key ];
|
||||
if ( type instanceof Function ) {
|
||||
properties[ key ] = type( value );
|
||||
continue;
|
||||
}
|
||||
|
||||
properties[ key ] = this.convertTo( value, type );
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given value into the requested type.
|
||||
*
|
||||
* @param {*} value The value to transform.
|
||||
* @param {PropertyType} type The type to transform it into.
|
||||
* @return {*} The converted type.
|
||||
* @private
|
||||
*/
|
||||
private convertTo( value: any, type: PropertyType ): PropertyTypeTypes | PropertyTypeTypes[] {
|
||||
if ( Array.isArray( value ) ) {
|
||||
return value.map( ( v: string ) => this.convertTo( v, type ) as PropertyTypeTypes );
|
||||
}
|
||||
|
||||
if ( null === value ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch ( type ) {
|
||||
case PropertyType.String: return String( value );
|
||||
case PropertyType.Integer: return parseInt( value );
|
||||
case PropertyType.Float: return parseFloat( value );
|
||||
case PropertyType.Boolean: return Boolean( value );
|
||||
case PropertyType.Date:
|
||||
return new Date( value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given type into a string.
|
||||
*
|
||||
* @param {*} value The value to transform.
|
||||
* @param {PropertyType} type The type to transform it into.
|
||||
* @return {*} The converted type.
|
||||
* @private
|
||||
*/
|
||||
private convertFrom( value: PropertyTypeTypes | PropertyTypeTypes[], type: PropertyType ): any {
|
||||
if ( Array.isArray( value ) ) {
|
||||
return value.map( ( v ) => this.convertFrom( v, type ) );
|
||||
}
|
||||
|
||||
if ( null === value ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch ( type ) {
|
||||
case PropertyType.String:
|
||||
case PropertyType.Integer:
|
||||
case PropertyType.Float:
|
||||
case PropertyType.Boolean:
|
||||
return String( value );
|
||||
|
||||
case PropertyType.Date: {
|
||||
return ( value as Date ).toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
import axios, { AxiosInstance } from 'axios';
|
||||
import * as moxios from 'moxios';
|
||||
import { AxiosURLToQueryInterceptor } from '../axios-url-to-query-interceptor';
|
||||
|
||||
describe( 'AxiosURLToQueryInterceptor', () => {
|
||||
let urlToQueryInterceptor: AxiosURLToQueryInterceptor;
|
||||
let axiosInstance: AxiosInstance;
|
||||
|
||||
beforeEach( () => {
|
||||
axiosInstance = axios.create();
|
||||
moxios.install( axiosInstance );
|
||||
urlToQueryInterceptor = new AxiosURLToQueryInterceptor( 'test' );
|
||||
urlToQueryInterceptor.start( axiosInstance );
|
||||
} );
|
||||
|
||||
afterEach( () => {
|
||||
urlToQueryInterceptor.stop( axiosInstance );
|
||||
moxios.uninstall();
|
||||
} );
|
||||
|
||||
it( 'should put path in query string', async () => {
|
||||
moxios.stubRequest( 'http://test.test/?test=%2Ftest%2Froute', {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
responseText: JSON.stringify( { test: 'value' } ),
|
||||
} );
|
||||
|
||||
const response = await axiosInstance.get( 'http://test.test/test/route' );
|
||||
|
||||
expect( response.status ).toEqual( 200 );
|
||||
} );
|
||||
} );
|
|
@ -1,4 +1,4 @@
|
|||
import { buildURL } from '../utils';
|
||||
import { buildURL, buildURLWithParams } from '../utils';
|
||||
|
||||
describe( 'buildURL', () => {
|
||||
it( 'should use base when given no url', () => {
|
||||
|
@ -15,9 +15,16 @@ describe( 'buildURL', () => {
|
|||
const url = buildURL( { baseURL: 'http://test.test', url: 'yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'should combine base and url with trailing/leading slashes', () => {
|
||||
const url = buildURL( { baseURL: 'http://test.test/////', url: '////yes/test' } );
|
||||
expect( url ).toBe( 'http://test.test/yes/test' );
|
||||
describe( 'buildURLWithParams', () => {
|
||||
it( 'should do nothing without query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test' } );
|
||||
expect( url ).toBe( 'http://test.test' );
|
||||
} );
|
||||
|
||||
it( 'should append query string', () => {
|
||||
const url = buildURLWithParams( { baseURL: 'http://test.test', params: { test: 'yes' } } );
|
||||
expect( url ).toBe( 'http://test.test?test=yes' );
|
||||
} );
|
||||
} );
|
||||
|
|
|
@ -2,10 +2,10 @@ import type { AxiosRequestConfig } from 'axios';
|
|||
import * as createHmac from 'create-hmac';
|
||||
import * as OAuth from 'oauth-1.0a';
|
||||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { buildURL } from './utils';
|
||||
import { buildURLWithParams } from './utils';
|
||||
|
||||
/**
|
||||
* A utility class for managing the lifecycle of an authentication interceptor.
|
||||
* An interceptor for adding OAuth 1.0a signatures to HTTP requests.
|
||||
*/
|
||||
export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
|
@ -14,7 +14,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @type {Object}
|
||||
* @private
|
||||
*/
|
||||
private oauth: OAuth;
|
||||
private readonly oauth: OAuth;
|
||||
|
||||
/**
|
||||
* Creates a new interceptor.
|
||||
|
@ -44,7 +44,7 @@ export class AxiosOAuthInterceptor extends AxiosInterceptor {
|
|||
* @return {AxiosRequestConfig} The request with the additional authorization headers.
|
||||
*/
|
||||
protected handleRequest( request: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = buildURL( request );
|
||||
const url = buildURLWithParams( request );
|
||||
if ( url.startsWith( 'https' ) ) {
|
||||
request.auth = {
|
||||
username: this.oauth.consumer.key,
|
||||
|
|
|
@ -2,6 +2,9 @@ import { AxiosResponse } from 'axios';
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { HTTPResponse } from '../http-client';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the responses from axios into a consistent format for package consumers.
|
||||
*/
|
||||
export class AxiosResponseInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* Transforms the Axios response into our HTTP response.
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { AxiosInterceptor } from './axios-interceptor';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { buildURL } from './utils';
|
||||
|
||||
/**
|
||||
* An interceptor for transforming the request's path into a query parameter.
|
||||
*/
|
||||
export class AxiosURLToQueryInterceptor extends AxiosInterceptor {
|
||||
/**
|
||||
* The query parameter we want to assign the path to.
|
||||
*
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
private readonly queryParam: string;
|
||||
|
||||
/**
|
||||
* Constructs a new interceptor.
|
||||
*
|
||||
* @param {string} queryParam The query parameter we want to assign the path to.
|
||||
*/
|
||||
public constructor( queryParam: string ) {
|
||||
super();
|
||||
|
||||
this.queryParam = queryParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the outgoing path into a query parameter.
|
||||
*
|
||||
* @param {AxiosRequestConfig} config The axios config.
|
||||
* @return {AxiosRequestConfig} The axios config.
|
||||
*/
|
||||
protected handleRequest( config: AxiosRequestConfig ): AxiosRequestConfig {
|
||||
const url = new URL( buildURL( config ) );
|
||||
|
||||
// Store the path in the query string.
|
||||
if ( config.params instanceof URLSearchParams ) {
|
||||
config.params.set( this.queryParam, url.pathname );
|
||||
} else if ( config.params ) {
|
||||
config.params[ this.queryParam ] = url.pathname;
|
||||
} else {
|
||||
config.params = { [ this.queryParam ]: url.pathname };
|
||||
}
|
||||
|
||||
// Store the URL without the path now that it's in the query string.
|
||||
url.pathname = '';
|
||||
config.url = url.toString();
|
||||
delete config.baseURL;
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,10 @@
|
|||
import { AxiosRequestConfig } from 'axios';
|
||||
|
||||
// @ts-ignore
|
||||
import buildFullPath = require( 'axios/lib/core/buildFullPath' );
|
||||
// @ts-ignore
|
||||
import appendParams = require( 'axios/lib/helpers/buildURL' );
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request.
|
||||
|
@ -8,17 +13,16 @@ import { AxiosRequestConfig } from 'axios';
|
|||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURL( request: AxiosRequestConfig ): string {
|
||||
const base = request.baseURL || '';
|
||||
if ( ! request.url ) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// Axios ignores the base when the URL is absolute.
|
||||
const url = request.url;
|
||||
if ( ! base || url.match( /^([a-z][a-z\d+\-.]*:)?\/\/[^\/]/i ) ) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// Remove trailing slashes from the base and leading slashes from the URL so we can combine them consistently.
|
||||
return base.replace( /\/+$/, '' ) + '/' + url.replace( /^\/+/, '' );
|
||||
return buildFullPath( request.baseURL, request.url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an Axios request config this function generates the URL that Axios will
|
||||
* use to make the request with the query parameters included.
|
||||
*
|
||||
* @param {AxiosRequestConfig} request The Axios request we're building the URL for.
|
||||
* @return {string} The merged URL.
|
||||
*/
|
||||
export function buildURLWithParams( request: AxiosRequestConfig ): string {
|
||||
return appendParams( buildURL( request ), request.params, request.paramsSerializer );
|
||||
}
|
||||
|
|
|
@ -1,39 +1,141 @@
|
|||
import { HTTPClient } from './http-client';
|
||||
import { AxiosClient, AxiosOAuthInterceptor } from './axios';
|
||||
import { AxiosRequestConfig } from 'axios';
|
||||
import { AxiosInterceptor } from './axios/axios-interceptor';
|
||||
import { AxiosURLToQueryInterceptor } from './axios/axios-url-to-query-interceptor';
|
||||
|
||||
/**
|
||||
* A class for generating HTTPClient instances with desired configurations.
|
||||
* These types describe the shape of the different auth methods our factory supports.
|
||||
*/
|
||||
type OAuthMethod = {
|
||||
type: 'oauth',
|
||||
key: string,
|
||||
secret: string,
|
||||
};
|
||||
type BasicAuthMethod = {
|
||||
type: 'basic',
|
||||
username: string,
|
||||
password: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* An interface for describing the shape of a client to create using the factory.
|
||||
*/
|
||||
interface BuildParams {
|
||||
wpURL: string,
|
||||
useIndexPermalinks?: boolean,
|
||||
auth?: OAuthMethod | BasicAuthMethod,
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for generating an HTTPClient with a desired configuration.
|
||||
*/
|
||||
export class HTTPClientFactory {
|
||||
/**
|
||||
* Creates a new client instance prepared for basic auth.
|
||||
* The configuration object describing the client we're trying to create.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} username
|
||||
* @param {string} password
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @private
|
||||
*/
|
||||
public static withBasicAuth( apiURL: string, username: string, password: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{
|
||||
baseURL: apiURL,
|
||||
auth: { username, password },
|
||||
},
|
||||
);
|
||||
private clientConfig: BuildParams;
|
||||
|
||||
private constructor( wpURL: string ) {
|
||||
this.clientConfig = { wpURL };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client instance prepared for oauth.
|
||||
* Creates a new factory that can be used to build clients.
|
||||
*
|
||||
* @param {string} apiURL
|
||||
* @param {string} consumerKey
|
||||
* @param {string} consumerSecret
|
||||
* @return {HTTPClient} An HTTP client configured for OAuth requests.
|
||||
* @param {string} wpURL The root URL of the WordPress installation we're querying.
|
||||
* @return {HTTPClientFactory} The new factory instance.
|
||||
*/
|
||||
public static withOAuth( apiURL: string, consumerKey: string, consumerSecret: string ): HTTPClient {
|
||||
return new AxiosClient(
|
||||
{ baseURL: apiURL },
|
||||
[ new AxiosOAuthInterceptor( consumerKey, consumerSecret ) ],
|
||||
);
|
||||
public static build( wpURL: string ): HTTPClientFactory {
|
||||
return new HTTPClientFactory( wpURL );
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize OAuth.
|
||||
*
|
||||
* @param {string} key The OAuth consumer key to use.
|
||||
* @param {string} secret The OAuth consumer secret to use.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withOAuth( key: string, secret: string ): this {
|
||||
this.clientConfig.auth = { type: 'oauth', key, secret };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to utilize basic auth.
|
||||
*
|
||||
* @param {string} username The WordPress username to use.
|
||||
* @param {string} password The password for the WordPress user.
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withBasicAuth( username: string, password: string ): this {
|
||||
this.clientConfig.auth = { type: 'basic', username, password };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use index permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the client to use query permalinks.
|
||||
*
|
||||
* @return {HTTPClientFactory} This factory.
|
||||
*/
|
||||
public withoutIndexPermalinks(): this {
|
||||
this.clientConfig.useIndexPermalinks = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a client instance using the configuration stored within.
|
||||
*
|
||||
* @return {HTTPClient} The created client.
|
||||
*/
|
||||
public create(): HTTPClient {
|
||||
const axiosConfig: AxiosRequestConfig = {};
|
||||
const interceptors: AxiosInterceptor[] = [];
|
||||
|
||||
axiosConfig.baseURL = this.clientConfig.wpURL;
|
||||
if ( ! axiosConfig.baseURL.endsWith( '/' ) ) {
|
||||
axiosConfig.baseURL += '/';
|
||||
}
|
||||
|
||||
if ( this.clientConfig.useIndexPermalinks ) {
|
||||
axiosConfig.baseURL += 'wp-json/';
|
||||
} else {
|
||||
interceptors.push( new AxiosURLToQueryInterceptor( 'rest_route' ) );
|
||||
}
|
||||
|
||||
if ( this.clientConfig.auth ) {
|
||||
switch ( this.clientConfig.auth.type ) {
|
||||
case 'basic':
|
||||
axiosConfig.auth = {
|
||||
username: this.clientConfig.auth.username,
|
||||
password: this.clientConfig.auth.password,
|
||||
};
|
||||
break;
|
||||
|
||||
case 'oauth':
|
||||
interceptors.push(
|
||||
new AxiosOAuthInterceptor(
|
||||
this.clientConfig.auth.key,
|
||||
this.clientConfig.auth.secret,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new AxiosClient( axiosConfig, interceptors );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,32 @@
|
|||
import { Model } from '../model';
|
||||
import { MetaData, PostStatus } from '../shared-types';
|
||||
import {
|
||||
BackorderStatus,
|
||||
CatalogVisibility,
|
||||
ProductAttribute,
|
||||
ProductDownload,
|
||||
ProductImage,
|
||||
ProductTerm, StockStatus,
|
||||
Taxability,
|
||||
} from './shared-types';
|
||||
|
||||
/**
|
||||
* The common parameters that all products can use in search.
|
||||
*/
|
||||
export type ProductSearchParams = { search: string };
|
||||
|
||||
/**
|
||||
* The common parameters that all products can update.
|
||||
*/
|
||||
export type ProductUpdateParams = 'name' | 'slug' | 'created' | 'postStatus' | 'shortDescription'
|
||||
| 'description' | 'sku' | 'categories' | 'tags' | 'isFeatured'
|
||||
| 'isVirtual' | 'attributes' | 'images' | 'catalogVisibility'
|
||||
| 'regularPrice' | 'onePerOrder' | 'taxStatus' | 'taxClass'
|
||||
| 'salePrice' | 'saleStart' | 'saleEnd' | 'isDownloadable'
|
||||
| 'downloadLimit' | 'daysToDownload' | 'weight' | 'length'
|
||||
| 'width' | 'height' | 'trackInventory' | 'remainingStock'
|
||||
| 'stockStatus' | 'backorderStatus' | 'allowReviews'
|
||||
| 'metaData';
|
||||
|
||||
/**
|
||||
* The base class for all product types.
|
||||
|
@ -11,10 +39,325 @@ export abstract class AbstractProduct extends Model {
|
|||
*/
|
||||
public readonly name: string = '';
|
||||
|
||||
/**
|
||||
* The slug of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly slug: string = '';
|
||||
|
||||
/**
|
||||
* The permalink of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly permalink: string = '';
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product was created.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly created: Date = new Date();
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product was last modified.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly modified: Date = new Date();
|
||||
|
||||
/**
|
||||
* The product's current post status.
|
||||
*
|
||||
* @type {PostStatus}
|
||||
*/
|
||||
public readonly postStatus: PostStatus = '';
|
||||
|
||||
/**
|
||||
* The product's short description.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly shortDescription: string = '';
|
||||
|
||||
/**
|
||||
* The product's full description.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly description: string = '';
|
||||
|
||||
/**
|
||||
* The product's SKU.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly sku: string = '';
|
||||
|
||||
/**
|
||||
* An array of the categories this product is in.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductTerm>}
|
||||
*/
|
||||
public readonly categories: readonly ProductTerm[] = [];
|
||||
|
||||
/**
|
||||
* An array of the tags this product has.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductTerm>}
|
||||
*/
|
||||
public readonly tags: readonly ProductTerm[] = [];
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is currently able to be purchased.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isPurchasable: boolean = true;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product should be featured.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isFeatured: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates that the product is delivered virtually.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isVirtual: boolean = false;
|
||||
|
||||
/**
|
||||
* The attributes for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductAttribute>}
|
||||
*/
|
||||
public readonly attributes: readonly ProductAttribute[] = [];
|
||||
|
||||
/**
|
||||
* The images for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductImage>}
|
||||
*/
|
||||
public readonly images: readonly ProductImage[] = [];
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product should be visible in the catalog.
|
||||
*
|
||||
* @type {CatalogVisibility}
|
||||
*/
|
||||
public readonly catalogVisibility: CatalogVisibility = CatalogVisibility.Everywhere;
|
||||
|
||||
/**
|
||||
* The current price of the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly price: string = '';
|
||||
|
||||
/**
|
||||
* The regular price of the product when not discounted.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly regularPrice: string = '';
|
||||
|
||||
/**
|
||||
* Indicates that only one of a product may be held in the order at a time.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly onePerOrder: boolean = false;
|
||||
|
||||
/**
|
||||
* The taxability of the product.
|
||||
*
|
||||
* @type {Taxability}
|
||||
*/
|
||||
public readonly taxStatus: Taxability = Taxability.ProductAndShipping;
|
||||
|
||||
/**
|
||||
* The tax class of the product
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly taxClass: string = '';
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is currently on sale.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly onSale: boolean = false;
|
||||
|
||||
/**
|
||||
* The price of the product when on sale.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly salePrice: string = '';
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product should start to be on sale.
|
||||
*
|
||||
* @type {Date|null}
|
||||
*/
|
||||
public readonly saleStart: Date | null = null;
|
||||
|
||||
/**
|
||||
* The GMT datetime when the product should no longer be on sale.
|
||||
*
|
||||
* @type {Date|null}
|
||||
*/
|
||||
public readonly saleEnd: Date | null = null;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the product is downloadable.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isDownloadable: boolean = false;
|
||||
|
||||
/**
|
||||
* The downloads available for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<ProductDownload>}
|
||||
*/
|
||||
public readonly downloads: readonly ProductDownload[] = [];
|
||||
|
||||
/**
|
||||
* The maximum number of times a customer may download the product's contents.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly downloadLimit: number = -1;
|
||||
|
||||
/**
|
||||
* The number of days after purchase that a customer may still download the product's contents.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly daysToDownload: number = -1;
|
||||
|
||||
/**
|
||||
* The weight of the product in the store's current units.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly weight: string = '';
|
||||
|
||||
/**
|
||||
* The length of the product in the store's current units.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly length: string = '';
|
||||
|
||||
/**
|
||||
* The width of the product in the store's current units.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly width: string = '';
|
||||
|
||||
/**
|
||||
* The height of the product in the store's current units.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly height: string = '';
|
||||
|
||||
/**
|
||||
* Indicates that the product must be shipped.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly requiresShipping: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates that the product's shipping is taxable.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isShippingTaxable: boolean = false;
|
||||
|
||||
/**
|
||||
* The shipping class for the product.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly shippingClass: string = '';
|
||||
|
||||
/**
|
||||
* Indicates that a product should use the inventory system.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly trackInventory: boolean = false;
|
||||
|
||||
/**
|
||||
* The number of inventory units remaining for this product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly remainingStock: number = -1;
|
||||
|
||||
/**
|
||||
* The product's stock status.
|
||||
*
|
||||
* @type {StockStatus}
|
||||
*/
|
||||
public readonly stockStatus: StockStatus = ''
|
||||
|
||||
/**
|
||||
* The status of backordering for a product.
|
||||
*
|
||||
* @type {BackorderStatus}
|
||||
*/
|
||||
public readonly backorderStatus: BackorderStatus = BackorderStatus.Allowed;
|
||||
|
||||
/**
|
||||
* Indicates whether or not a product can be backordered.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly canBackorder: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates whether or not a product is on backorder.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isOnBackorder: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates whether or not a product allows reviews.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly allowReviews: boolean = false;
|
||||
|
||||
/**
|
||||
* The average rating for the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly averageRating: number = -1;
|
||||
|
||||
/**
|
||||
* The number of ratings for the product.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly numRatings: number = -1;
|
||||
|
||||
/**
|
||||
* The extra metadata for the product.
|
||||
*
|
||||
* @type {ReadonlyArray.<MetaData>}
|
||||
*/
|
||||
public readonly metaData: readonly MetaData[] = [];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,261 @@
|
|||
/**
|
||||
* An enum describing the catalog visibility options for products.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum CatalogVisibility {
|
||||
/**
|
||||
* The product should be visible everywhere.
|
||||
*/
|
||||
Everywhere = 'visible',
|
||||
|
||||
/**
|
||||
* The product should only be visible in the shop catalog.
|
||||
*/
|
||||
ShopOnly = 'catalog',
|
||||
|
||||
/**
|
||||
* The product should only be visible in search results.
|
||||
*/
|
||||
SearchOnly = 'search',
|
||||
|
||||
/**
|
||||
* The product should be hidden everywhere.
|
||||
*/
|
||||
Hidden = 'hidden'
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the taxability of a product.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum Taxability {
|
||||
/**
|
||||
* The product and shipping are both taxable.
|
||||
*/
|
||||
ProductAndShipping = 'taxable',
|
||||
|
||||
/**
|
||||
* Only the product's shipping is taxable.
|
||||
*/
|
||||
ShippingOnly = 'shipping',
|
||||
|
||||
/**
|
||||
* The product and shipping are not taxable.
|
||||
*/
|
||||
None = 'none'
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates the status for backorders for a product.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export enum BackorderStatus {
|
||||
/**
|
||||
* The product is allowed to be backordered.
|
||||
*/
|
||||
Allowed = 'yes',
|
||||
|
||||
/**
|
||||
* The product is allowed to be backordered but it will notify the customer of that fact.
|
||||
*/
|
||||
AllowedWithNotification = 'notify',
|
||||
|
||||
/**
|
||||
* The product is not allowed to be backordered.
|
||||
*/
|
||||
NotAllowed = 'no'
|
||||
}
|
||||
|
||||
/**
|
||||
* A product's stock status.
|
||||
*
|
||||
* @typedef StockStatus
|
||||
* @alias 'instock'|'outofstock'|'onbackorder'|string
|
||||
*/
|
||||
export type StockStatus = 'instock' | 'outofstock' | 'onbackorder' | string
|
||||
|
||||
/**
|
||||
* A products taxonomy term such as categories or tags.
|
||||
*/
|
||||
export class ProductTerm {
|
||||
/**
|
||||
* The ID of the term.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly id: number = -1;
|
||||
|
||||
/**
|
||||
* The name of the term.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly name: string = '';
|
||||
|
||||
/**
|
||||
* The slug of the term.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly slug: string = '';
|
||||
|
||||
/**
|
||||
* Creates a new product term.
|
||||
*
|
||||
* @param {Partial.<ProductTerm>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductTerm > ) {
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A product's download.
|
||||
*/
|
||||
export class ProductDownload {
|
||||
/**
|
||||
* The ID of the downloadable file.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly id: string = '';
|
||||
|
||||
/**
|
||||
* The name of the downloadable file.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly name: string = '';
|
||||
|
||||
/**
|
||||
* The URL of the downloadable file.
|
||||
*
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly url: string = '';
|
||||
|
||||
/**
|
||||
* Creates a new product download.
|
||||
*
|
||||
* @param {Partial.<ProductDownload>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductDownload > ) {
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A product's attributes.
|
||||
*/
|
||||
export class ProductAttribute {
|
||||
/**
|
||||
* The ID of the attribute.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly id: number = -1;
|
||||
|
||||
/**
|
||||
* The name of the attribute.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly name: string = '';
|
||||
|
||||
/**
|
||||
* The sort order of the attribute.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly sortOrder: number = -1;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the attribute is visible on the product page.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isVisibleOnProductPage: boolean = false;
|
||||
|
||||
/**
|
||||
* Indicates whether or not the attribute should be used in variations.
|
||||
*
|
||||
* @type {boolean}
|
||||
*/
|
||||
public readonly isForVariations: boolean = false;
|
||||
|
||||
/**
|
||||
* The options which are available for the attribute.
|
||||
*
|
||||
* @type {ReadonlyArray.<string>}
|
||||
*/
|
||||
public readonly options: readonly string[] = [];
|
||||
|
||||
/**
|
||||
* Creates a new product attribute.
|
||||
*
|
||||
* @param {Partial.<ProductAttribute>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductAttribute > ) {
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A product's image.
|
||||
*/
|
||||
export class ProductImage {
|
||||
/**
|
||||
* The ID of the image.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
public readonly id: number = -1;
|
||||
|
||||
/**
|
||||
* The GMT datetime when the image was created.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly created: Date = new Date();
|
||||
|
||||
/**
|
||||
* The GMT datetime when the image was last modified.
|
||||
*
|
||||
* @type {Date}
|
||||
*/
|
||||
public readonly modified: Date = new Date();
|
||||
|
||||
/**
|
||||
* The URL for the image file.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly url: string = '';
|
||||
|
||||
/**
|
||||
* The name of the image file.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly name: string = '';
|
||||
|
||||
/**
|
||||
* The alt text to use on the image.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly altText: string = '';
|
||||
|
||||
/**
|
||||
* Creates a new product image.
|
||||
*
|
||||
* @param {Partial.<ProductImage>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< ProductImage > ) {
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
|
@ -1,13 +1,27 @@
|
|||
import { AbstractProduct } from './abstract-product';
|
||||
import { AbstractProduct, ProductSearchParams, ProductUpdateParams } from './abstract-product';
|
||||
import { HTTPClient } from '../../http';
|
||||
import { simpleProductRESTRepository } from '../../repositories/rest/products/simple-product';
|
||||
import { CreatesModels, ModelRepositoryParams } from '../../framework/model-repository';
|
||||
import {
|
||||
CreatesModels,
|
||||
DeletesModels, ListsModels,
|
||||
ModelRepositoryParams,
|
||||
ReadsModels,
|
||||
UpdatesModels,
|
||||
} from '../../framework/model-repository';
|
||||
|
||||
/**
|
||||
* The parameters embedded in this generic can be used in the ModelRepository in order to give
|
||||
* type-safety in an incredibly granular way.
|
||||
*/
|
||||
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, never, 'regularPrice' >;
|
||||
export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct, never, ProductSearchParams, ProductUpdateParams >;
|
||||
|
||||
/**
|
||||
* An interface for listing simple products using the repository.
|
||||
*
|
||||
* @typedef ListsSimpleProducts
|
||||
* @alias ListsModels.<SimpleProduct>
|
||||
*/
|
||||
export type ListsSimpleProducts = ListsModels< SimpleProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for creating simple products using the repository.
|
||||
|
@ -17,6 +31,30 @@ export type SimpleProductRepositoryParams = ModelRepositoryParams< SimpleProduct
|
|||
*/
|
||||
export type CreatesSimpleProducts = CreatesModels< SimpleProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for reading simple products using the repository.
|
||||
*
|
||||
* @typedef ReadsSimpleProducts
|
||||
* @alias ReadsModels.<SimpleProduct>
|
||||
*/
|
||||
export type ReadsSimpleProducts = ReadsModels< SimpleProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for updating simple products using the repository.
|
||||
*
|
||||
* @typedef UpdatesSimpleProducts
|
||||
* @alias UpdatesModels.<SimpleProduct>
|
||||
*/
|
||||
export type UpdatesSimpleProducts = UpdatesModels< SimpleProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* An interface for deleting simple products using the repository.
|
||||
*
|
||||
* @typedef DeletesSimpleProducts
|
||||
* @alias DeletesModels.<SimpleProduct>
|
||||
*/
|
||||
export type DeletesSimpleProducts = DeletesModels< SimpleProductRepositoryParams >;
|
||||
|
||||
/**
|
||||
* A simple product object.
|
||||
*/
|
||||
|
@ -26,7 +64,7 @@ export class SimpleProduct extends AbstractProduct {
|
|||
*
|
||||
* @param {Object} properties The properties to set in the object.
|
||||
*/
|
||||
public constructor( properties: Partial< SimpleProduct > = {} ) {
|
||||
public constructor( properties?: Partial< SimpleProduct > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ export class SettingGroup extends Model {
|
|||
*
|
||||
* @param {Object} properties The properties to set in the object.
|
||||
*/
|
||||
public constructor( properties: Partial< SettingGroup > = {} ) {
|
||||
public constructor( properties?: Partial< SettingGroup > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
|
|
|
@ -94,7 +94,7 @@ export class Setting extends Model {
|
|||
*
|
||||
* @param {Object} properties The properties to set in the object.
|
||||
*/
|
||||
public constructor( properties: Partial< Setting > = {} ) {
|
||||
public constructor( properties?: Partial< Setting > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import { Model } from './model';
|
||||
|
||||
/**
|
||||
* A constructor for a model.
|
||||
*
|
||||
* @typedef ModelConstructor
|
||||
* @alias Function.<T>
|
||||
* @template T
|
||||
*/
|
||||
export type ModelConstructor< T extends Model > = new ( properties: Partial< T > ) => T;
|
||||
|
||||
/**
|
||||
* A post's status.
|
||||
*
|
||||
* @typedef PostStatus
|
||||
* @alias 'draft'|'pending'|'private'|'publish'|string
|
||||
*/
|
||||
export type PostStatus = 'draft' | 'pending' | 'private' | 'publish' | string;
|
||||
|
||||
/**
|
||||
* A metadata object.
|
||||
*/
|
||||
export class MetaData extends Model {
|
||||
/**
|
||||
* The key of the metadata.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
public readonly key: string = '';
|
||||
|
||||
/**
|
||||
* The value of the metadata.
|
||||
*
|
||||
* @type {*}
|
||||
*/
|
||||
public readonly value: any = '';
|
||||
|
||||
/**
|
||||
* Creates a new metadata.
|
||||
*
|
||||
* @param {Partial.<MetaData>} properties The properties to set.
|
||||
*/
|
||||
public constructor( properties?: Partial< MetaData > ) {
|
||||
super();
|
||||
Object.assign( this, properties );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,247 @@
|
|||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { HTTPClient, HTTPResponse } from '../../../http';
|
||||
import { ModelTransformer } from '../../../framework/model-transformer';
|
||||
import { DummyModel } from '../../../__test_data__/dummy-model';
|
||||
import {
|
||||
restCreate,
|
||||
restDelete, restDeleteChild,
|
||||
restList,
|
||||
restListChild,
|
||||
restRead,
|
||||
restReadChild,
|
||||
restUpdate,
|
||||
restUpdateChild,
|
||||
} from '../shared';
|
||||
import { ModelRepositoryParams } from '../../../framework/model-repository';
|
||||
import { Model } from '../../../models/model';
|
||||
|
||||
type DummyModelParams = ModelRepositoryParams< DummyModel, never, { search: string }, 'name' >
|
||||
|
||||
class DummyChildModel extends Model {
|
||||
public childName: string = '';
|
||||
|
||||
public constructor( partial?: Partial< DummyModel > ) {
|
||||
super();
|
||||
Object.assign( this, partial );
|
||||
}
|
||||
}
|
||||
type DummyChildParams = ModelRepositoryParams< DummyChildModel, { parent: string }, { childSearch: string }, 'childName' >
|
||||
|
||||
describe( 'Shared REST Functions', () => {
|
||||
let mockClient: MockProxy< HTTPClient >;
|
||||
let mockTransformer: MockProxy< ModelTransformer< any > > & ModelTransformer< any >;
|
||||
|
||||
beforeEach( () => {
|
||||
mockClient = mock< HTTPClient >();
|
||||
mockTransformer = mock< ModelTransformer< any > >();
|
||||
} );
|
||||
|
||||
it( 'restList', async () => {
|
||||
mockClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
[
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
{
|
||||
id: 'Test-2',
|
||||
label: 'Test 2',
|
||||
},
|
||||
],
|
||||
) );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restList< DummyModelParams >( () => 'test-url', DummyModel, mockClient, mockTransformer );
|
||||
|
||||
const result = await fn( { search: 'Test' } );
|
||||
|
||||
expect( result ).toHaveLength( 2 );
|
||||
expect( result[ 0 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( result[ 1 ] ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url', { search: 'Test' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-2', label: 'Test 2' } );
|
||||
} );
|
||||
|
||||
it( 'restListChildren', async () => {
|
||||
mockClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
[
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
{
|
||||
id: 'Test-2',
|
||||
label: 'Test 2',
|
||||
},
|
||||
],
|
||||
) );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restListChild< DummyChildParams >(
|
||||
( parent ) => 'test-url-' + parent.parent,
|
||||
DummyChildModel,
|
||||
mockClient,
|
||||
mockTransformer,
|
||||
);
|
||||
|
||||
const result = await fn( { parent: '123' }, { childSearch: 'Test' } );
|
||||
|
||||
expect( result ).toHaveLength( 2 );
|
||||
expect( result[ 0 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
|
||||
expect( result[ 1 ] ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
|
||||
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123', { childSearch: 'Test' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-2', label: 'Test 2' } );
|
||||
} );
|
||||
|
||||
it( 'restCreate', async () => {
|
||||
mockClient.post.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
) );
|
||||
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restCreate< DummyModelParams >(
|
||||
( properties ) => 'test-url-' + properties.name,
|
||||
DummyModel,
|
||||
mockClient,
|
||||
mockTransformer,
|
||||
);
|
||||
|
||||
const result = await fn( { name: 'Test' } );
|
||||
|
||||
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
|
||||
expect( mockClient.post ).toHaveBeenCalledWith( 'test-url-Test', { name: 'From-Test' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
} );
|
||||
|
||||
it( 'restRead', async () => {
|
||||
mockClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
) );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restRead< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
|
||||
|
||||
const result = await fn( 123 );
|
||||
|
||||
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123' );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
} );
|
||||
|
||||
it( 'restReadChildren', async () => {
|
||||
mockClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
) );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restReadChild< DummyChildParams >(
|
||||
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
|
||||
DummyChildModel,
|
||||
mockClient,
|
||||
mockTransformer,
|
||||
);
|
||||
|
||||
const result = await fn( { parent: '123' }, 456 );
|
||||
|
||||
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( mockClient.get ).toHaveBeenCalledWith( 'test-url-123-456' );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
} );
|
||||
|
||||
it( 'restUpdate', async () => {
|
||||
mockClient.patch.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
) );
|
||||
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restUpdate< DummyModelParams >( ( id ) => 'test-url-' + id, DummyModel, mockClient, mockTransformer );
|
||||
|
||||
const result = await fn( 123, { name: 'Test' } );
|
||||
|
||||
expect( result ).toMatchObject( new DummyModel( { name: 'Test' } ) );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { name: 'Test' } );
|
||||
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123', { name: 'From-Test' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
} );
|
||||
|
||||
it( 'restUpdateChildren', async () => {
|
||||
mockClient.patch.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'Test-1',
|
||||
label: 'Test 1',
|
||||
},
|
||||
) );
|
||||
mockTransformer.fromModel.mockReturnValue( { name: 'From-Test' } );
|
||||
mockTransformer.toModel.mockReturnValue( new DummyChildModel( { name: 'Test' } ) );
|
||||
|
||||
const fn = restUpdateChild< DummyChildParams >(
|
||||
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
|
||||
DummyChildModel,
|
||||
mockClient,
|
||||
mockTransformer,
|
||||
);
|
||||
|
||||
const result = await fn( { parent: '123' }, 456, { childName: 'Test' } );
|
||||
|
||||
expect( result ).toMatchObject( new DummyChildModel( { name: 'Test' } ) );
|
||||
expect( mockTransformer.fromModel ).toHaveBeenCalledWith( { childName: 'Test' } );
|
||||
expect( mockClient.patch ).toHaveBeenCalledWith( 'test-url-123-456', { name: 'From-Test' } );
|
||||
expect( mockTransformer.toModel ).toHaveBeenCalledWith( DummyChildModel, { id: 'Test-1', label: 'Test 1' } );
|
||||
} );
|
||||
|
||||
it( 'restDelete', async () => {
|
||||
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
|
||||
|
||||
const fn = restDelete< DummyModelParams >( ( id ) => 'test-url-' + id, mockClient );
|
||||
|
||||
const result = await fn( 123 );
|
||||
|
||||
expect( result ).toBe( true );
|
||||
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123' );
|
||||
} );
|
||||
|
||||
it( 'restDeleteChildren', async () => {
|
||||
mockClient.delete.mockResolvedValue( new HTTPResponse( 200, {}, {} ) );
|
||||
|
||||
const fn = restDeleteChild< DummyChildParams >(
|
||||
( parent, id ) => 'test-url-' + parent.parent + '-' + id,
|
||||
mockClient,
|
||||
);
|
||||
|
||||
const result = await fn( { parent: '123' }, 456 );
|
||||
|
||||
expect( result ).toBe( true );
|
||||
expect( mockClient.delete ).toHaveBeenCalledWith( 'test-url-123-456' );
|
||||
} );
|
||||
} );
|
|
@ -1,28 +0,0 @@
|
|||
import { simpleProductRESTRepository } from '../simple-product';
|
||||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { HTTPClient, HTTPResponse } from '../../../../http';
|
||||
import { SimpleProduct } from '../../../../models';
|
||||
|
||||
describe( 'simpleProductRESTRepository', () => {
|
||||
let httpClient: MockProxy< HTTPClient >;
|
||||
let repository: ReturnType< typeof simpleProductRESTRepository >;
|
||||
|
||||
beforeEach( () => {
|
||||
httpClient = mock< HTTPClient >();
|
||||
repository = simpleProductRESTRepository( httpClient );
|
||||
} );
|
||||
|
||||
it( 'should create', async () => {
|
||||
httpClient.post.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{ id: 123 },
|
||||
) );
|
||||
|
||||
const created = await repository.create( { name: 'Test Product' } );
|
||||
|
||||
expect( created ).toBeInstanceOf( SimpleProduct );
|
||||
expect( created ).toMatchObject( { id: 123 } );
|
||||
expect( httpClient.post ).toHaveBeenCalledWith( '/wc/v3/products', { type: 'simple', name: 'Test Product' } );
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,221 @@
|
|||
import { ModelTransformation, ModelTransformer, TransformationOrder } from '../../../framework/model-transformer';
|
||||
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
|
||||
import { AbstractProduct } from '../../../models/products/abstract-product';
|
||||
import { AddPropertyTransformation } from '../../../framework/transformations/add-property-transformation';
|
||||
import { IgnorePropertyTransformation } from '../../../framework/transformations/ignore-property-transformation';
|
||||
import {
|
||||
PropertyType,
|
||||
PropertyTypeTransformation,
|
||||
} from '../../../framework/transformations/property-type-transformation';
|
||||
import { CustomTransformation } from '../../../framework/transformations/custom-transformation';
|
||||
import { ProductAttribute, ProductDownload, ProductImage, ProductTerm } from '../../../models/products/shared-types';
|
||||
import { ModelTransformerTransformation } from '../../../framework/transformations/model-transformer-transformation';
|
||||
import { MetaData } from '../../../models/shared-types';
|
||||
import { createMetaDataTransformer } from '../shared';
|
||||
|
||||
/**
|
||||
* Creates a transformer for the product term object.
|
||||
*
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
function createProductTermTransformer(): ModelTransformer< ProductTerm > {
|
||||
return new ModelTransformer(
|
||||
[
|
||||
new PropertyTypeTransformation( { id: PropertyType.Integer } ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the product attribute object.
|
||||
*
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
function createProductAttributeTransformer(): ModelTransformer< ProductAttribute > {
|
||||
return new ModelTransformer(
|
||||
[
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
id: PropertyType.Integer,
|
||||
sortOrder: PropertyType.Integer,
|
||||
isVisibleOnProductPage: PropertyType.Boolean,
|
||||
isForVariations: PropertyType.Boolean,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< ProductAttribute >(
|
||||
{
|
||||
sortOrder: 'position',
|
||||
isVisibleOnProductPage: 'visible',
|
||||
isForVariations: 'variation',
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the product image object.
|
||||
*
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
function createProductImageTransformer(): ModelTransformer< ProductImage > {
|
||||
return new ModelTransformer(
|
||||
[
|
||||
new IgnorePropertyTransformation( [ 'date_created', 'date_modified' ] ),
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
id: PropertyType.Integer,
|
||||
created: PropertyType.Date,
|
||||
modified: PropertyType.Date,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< ProductImage >(
|
||||
{
|
||||
created: 'date_created_gmt',
|
||||
modified: 'date_modified_gmt',
|
||||
url: 'src',
|
||||
altText: 'altText',
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the product download object.
|
||||
*
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
function createProductDownloadTransformer(): ModelTransformer< ProductDownload > {
|
||||
return new ModelTransformer(
|
||||
[
|
||||
new KeyChangeTransformation< ProductDownload >( { url: 'file' } ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transformer for the shared properties of all products.
|
||||
*
|
||||
* @param {string} type The product type.
|
||||
* @param {Array.<ModelTransformation>} transformations Optional transformers to add to the transformer.
|
||||
* @return {ModelTransformer} The created transformer.
|
||||
*/
|
||||
export function createProductTransformer< T extends AbstractProduct >(
|
||||
type: string,
|
||||
transformations?: ModelTransformation[],
|
||||
): ModelTransformer< T > {
|
||||
if ( ! transformations ) {
|
||||
transformations = [];
|
||||
}
|
||||
|
||||
transformations.push(
|
||||
new AddPropertyTransformation( {}, { type } ),
|
||||
new IgnorePropertyTransformation(
|
||||
[
|
||||
'date_created',
|
||||
'date_modified',
|
||||
'date_on_sale_from',
|
||||
'date_on_sale_to',
|
||||
],
|
||||
),
|
||||
new ModelTransformerTransformation( 'categories', ProductTerm, createProductTermTransformer() ),
|
||||
new ModelTransformerTransformation( 'tags', ProductTerm, createProductTermTransformer() ),
|
||||
new ModelTransformerTransformation( 'attributes', ProductAttribute, createProductAttributeTransformer() ),
|
||||
new ModelTransformerTransformation( 'images', ProductImage, createProductImageTransformer() ),
|
||||
new ModelTransformerTransformation( 'downloads', ProductDownload, createProductDownloadTransformer() ),
|
||||
new ModelTransformerTransformation( 'metaData', MetaData, createMetaDataTransformer() ),
|
||||
new CustomTransformation(
|
||||
TransformationOrder.Normal,
|
||||
( properties: any ) => {
|
||||
if ( properties.hasOwnProperty( 'dimensions' ) ) {
|
||||
properties.length = properties.dimensions.length;
|
||||
properties.width = properties.dimensions.width;
|
||||
properties.height = properties.dimensions.height;
|
||||
delete properties.dimensions;
|
||||
}
|
||||
|
||||
return properties;
|
||||
},
|
||||
( properties: any ) => {
|
||||
if ( properties.hasOwnProperty( 'length ' ) ||
|
||||
properties.hasOwnProperty( 'width' ) ||
|
||||
properties.hasOwnProperty( 'height' ) ) {
|
||||
properties.dimensions = {
|
||||
length: properties.length,
|
||||
width: properties.width,
|
||||
height: properties.height,
|
||||
};
|
||||
delete properties.length;
|
||||
delete properties.width;
|
||||
delete properties.height;
|
||||
}
|
||||
|
||||
return properties;
|
||||
},
|
||||
),
|
||||
new PropertyTypeTransformation(
|
||||
{
|
||||
created: PropertyType.Date,
|
||||
modified: PropertyType.Date,
|
||||
isPurchasable: PropertyType.Boolean,
|
||||
isFeatured: PropertyType.Boolean,
|
||||
isVirtual: PropertyType.Boolean,
|
||||
onePerOrder: PropertyType.Boolean,
|
||||
onSale: PropertyType.Boolean,
|
||||
saleStart: PropertyType.Date,
|
||||
saleEnd: PropertyType.Date,
|
||||
isDownloadable: PropertyType.Boolean,
|
||||
downloadLimit: PropertyType.Integer,
|
||||
daysToDownload: PropertyType.Integer,
|
||||
requiresShipping: PropertyType.Boolean,
|
||||
isShippingTaxable: PropertyType.Boolean,
|
||||
trackInventory: PropertyType.Boolean,
|
||||
remainingStock: PropertyType.Integer,
|
||||
canBackorder: PropertyType.Boolean,
|
||||
isOnBackorder: PropertyType.Boolean,
|
||||
allowReviews: PropertyType.Boolean,
|
||||
averageRating: PropertyType.Integer,
|
||||
numRatings: PropertyType.Integer,
|
||||
},
|
||||
),
|
||||
new KeyChangeTransformation< AbstractProduct >(
|
||||
{
|
||||
created: 'date_created_gmt',
|
||||
modified: 'date_modified_gmt',
|
||||
postStatus: 'status',
|
||||
shortDescription: 'short_description',
|
||||
isPurchasable: 'purchasable',
|
||||
isFeatured: 'featured',
|
||||
isVirtual: 'virtual',
|
||||
catalogVisibility: 'catalog_visibility',
|
||||
regularPrice: 'regular_price',
|
||||
onePerOrder: 'sold_individually',
|
||||
taxStatus: 'tax_status',
|
||||
taxClass: 'tax_class',
|
||||
onSale: 'on_sale',
|
||||
salePrice: 'sale_price',
|
||||
saleStart: 'date_on_sale_from_gmt',
|
||||
saleEnd: 'date_on_sale_to_gmt',
|
||||
isDownloadable: 'downloadable',
|
||||
downloadLimit: 'download_limit',
|
||||
daysToDownload: 'download_expiry',
|
||||
requiresShipping: 'shipping_required',
|
||||
isShippingTaxable: 'shipping_taxable',
|
||||
shippingClass: 'shipping_class',
|
||||
trackInventory: 'manage_stock',
|
||||
remainingStock: 'stock_quantity',
|
||||
stockStatus: 'stock_status',
|
||||
backorderStatus: 'backorders',
|
||||
canBackorder: 'backorders_allowed',
|
||||
isOnBackorder: 'backordered',
|
||||
allowReviews: 'reviews_allowed',
|
||||
averageRating: 'average_rating',
|
||||
numRatings: 'rating_count',
|
||||
metaData: 'meta_data',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return new ModelTransformer( transformations );
|
||||
}
|
|
@ -1,39 +1,43 @@
|
|||
import { HTTPClient } from '../../../http';
|
||||
import { CreateFn, ModelRepository } from '../../../framework/model-repository';
|
||||
import { ModelRepository } from '../../../framework/model-repository';
|
||||
import { SimpleProduct } from '../../../models';
|
||||
import { CreatesSimpleProducts, SimpleProductRepositoryParams } from '../../../models/products/simple-product';
|
||||
|
||||
function restCreate( httpClient: HTTPClient ): CreateFn< SimpleProductRepositoryParams > {
|
||||
return async ( properties ) => {
|
||||
const response = await httpClient.post(
|
||||
'/wc/v3/products',
|
||||
{
|
||||
type: 'simple',
|
||||
name: properties.name,
|
||||
regular_price: properties.regularPrice,
|
||||
},
|
||||
);
|
||||
|
||||
return Promise.resolve( new SimpleProduct( {
|
||||
id: response.data.id,
|
||||
name: response.data.name,
|
||||
regularPrice: response.data.regular_price,
|
||||
} ) );
|
||||
};
|
||||
}
|
||||
import {
|
||||
CreatesSimpleProducts,
|
||||
DeletesSimpleProducts,
|
||||
ListsSimpleProducts,
|
||||
ReadsSimpleProducts,
|
||||
SimpleProductRepositoryParams,
|
||||
UpdatesSimpleProducts,
|
||||
} from '../../../models/products/simple-product';
|
||||
import { createProductTransformer } from './shared';
|
||||
import { restCreate, restDelete, restList, restRead, restUpdate } from '../shared';
|
||||
import { ModelID } from '../../../models/model';
|
||||
|
||||
/**
|
||||
* Creates a new ModelRepository instance for interacting with models via the REST API.
|
||||
*
|
||||
* @param {HTTPClient} httpClient The HTTP client for the REST requests to be made using.
|
||||
* @return {CreatesSimpleProducts} The created repository.
|
||||
* @return {
|
||||
* ListsSimpleProducts|
|
||||
* CreatesSimpleProducts|
|
||||
* ReadsSimpleProducts|
|
||||
* UpdatesSimpleProducts|
|
||||
* DeletesSimpleProducts
|
||||
* } The created repository.
|
||||
*/
|
||||
export function simpleProductRESTRepository( httpClient: HTTPClient ): CreatesSimpleProducts {
|
||||
export function simpleProductRESTRepository( httpClient: HTTPClient ): ListsSimpleProducts
|
||||
& CreatesSimpleProducts
|
||||
& ReadsSimpleProducts
|
||||
& UpdatesSimpleProducts
|
||||
& DeletesSimpleProducts {
|
||||
const buildURL = ( id: ModelID ) => '/wc/v3/products/' + id;
|
||||
const transformer = createProductTransformer( 'simple' );
|
||||
|
||||
return new ModelRepository(
|
||||
null,
|
||||
restCreate( httpClient ),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
restList< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
|
||||
restCreate< SimpleProductRepositoryParams >( () => '/wc/v3/products', SimpleProduct, httpClient, transformer ),
|
||||
restRead< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
|
||||
restUpdate< SimpleProductRepositoryParams >( buildURL, SimpleProduct, httpClient, transformer ),
|
||||
restDelete< SimpleProductRepositoryParams >( buildURL, httpClient ),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { HTTPClient, HTTPResponse } from '../../../../http';
|
||||
import { settingGroupRESTRepository } from '../setting-group';
|
||||
|
||||
describe( 'settingGroupRESTRepository', () => {
|
||||
let httpClient: MockProxy< HTTPClient >;
|
||||
let repository: ReturnType< typeof settingGroupRESTRepository >;
|
||||
|
||||
beforeEach( () => {
|
||||
httpClient = mock< HTTPClient >();
|
||||
repository = settingGroupRESTRepository( httpClient );
|
||||
} );
|
||||
|
||||
it( 'should list', async () => {
|
||||
httpClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
[
|
||||
{
|
||||
id: 'group_1',
|
||||
label: 'Test Group 1',
|
||||
},
|
||||
{
|
||||
id: 'group_2',
|
||||
label: 'Test Group 2',
|
||||
},
|
||||
],
|
||||
) );
|
||||
|
||||
const list = await repository.list();
|
||||
|
||||
expect( list ).toHaveLength( 2 );
|
||||
expect( list[ 0 ] ).toMatchObject( { id: 'group_1', label: 'Test Group 1' } );
|
||||
expect( list[ 1 ] ).toMatchObject( { id: 'group_2', label: 'Test Group 2' } );
|
||||
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings' );
|
||||
} );
|
||||
} );
|
|
@ -1,73 +0,0 @@
|
|||
import { mock, MockProxy } from 'jest-mock-extended';
|
||||
import { HTTPClient, HTTPResponse } from '../../../../http';
|
||||
import { settingRESTRepository } from '../setting';
|
||||
|
||||
describe( 'settingGroupRESTRepository', () => {
|
||||
let httpClient: MockProxy< HTTPClient >;
|
||||
let repository: ReturnType< typeof settingRESTRepository >;
|
||||
|
||||
beforeEach( () => {
|
||||
httpClient = mock< HTTPClient >();
|
||||
repository = settingRESTRepository( httpClient );
|
||||
} );
|
||||
|
||||
it( 'should list', async () => {
|
||||
httpClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
[
|
||||
{
|
||||
id: 'setting_1',
|
||||
label: 'Test Setting 1',
|
||||
},
|
||||
{
|
||||
id: 'setting_2',
|
||||
label: 'Test Setting 2',
|
||||
},
|
||||
],
|
||||
) );
|
||||
|
||||
const list = await repository.list( 'general' );
|
||||
|
||||
expect( list ).toHaveLength( 2 );
|
||||
expect( list[ 0 ] ).toMatchObject( { id: 'setting_1', label: 'Test Setting 1' } );
|
||||
expect( list[ 1 ] ).toMatchObject( { id: 'setting_2', label: 'Test Setting 2' } );
|
||||
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general' );
|
||||
} );
|
||||
|
||||
it( 'should read', async () => {
|
||||
httpClient.get.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'setting_1',
|
||||
label: 'Test Setting',
|
||||
},
|
||||
) );
|
||||
|
||||
const read = await repository.read( 'general', 'setting_1' );
|
||||
|
||||
expect( read ).toMatchObject( { id: 'setting_1', label: 'Test Setting' } );
|
||||
expect( httpClient.get ).toHaveBeenCalledWith( '/wc/v3/settings/general/setting_1' );
|
||||
} );
|
||||
|
||||
it( 'should update', async () => {
|
||||
httpClient.patch.mockResolvedValue( new HTTPResponse(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
id: 'setting_1',
|
||||
label: 'Test Setting',
|
||||
value: 'updated-value',
|
||||
},
|
||||
) );
|
||||
|
||||
const updated = await repository.update( 'general', 'setting_1', { value: 'test-value' } );
|
||||
|
||||
expect( updated ).toMatchObject( { id: 'setting_1', value: 'updated-value' } );
|
||||
expect( httpClient.patch ).toHaveBeenCalledWith(
|
||||
'/wc/v3/settings/general/setting_1',
|
||||
{ value: 'test-value' },
|
||||
);
|
||||
} );
|
||||
} );
|
|
@ -1,24 +1,17 @@
|
|||
import { HTTPClient } from '../../../http';
|
||||
import { ListFn, ModelRepository } from '../../../framework/model-repository';
|
||||
import { ModelRepository } from '../../../framework/model-repository';
|
||||
import { SettingGroup } from '../../../models';
|
||||
import { ListsSettingGroups, SettingGroupRepositoryParams } from '../../../models/settings/setting-group';
|
||||
import { ModelTransformer } from '../../../framework/model-transformer';
|
||||
import { KeyChangeTransformation } from '../../../framework/transformations/key-change-transformation';
|
||||
import { restList } from '../shared';
|
||||
|
||||
function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParams > {
|
||||
return async () => {
|
||||
const response = await httpClient.get( '/wc/v3/settings' );
|
||||
|
||||
const list: SettingGroup[] = [];
|
||||
for ( const raw of response.data ) {
|
||||
list.push( new SettingGroup( {
|
||||
id: raw.id,
|
||||
label: raw.label,
|
||||
description: raw.description,
|
||||
parentID: raw.parent_id,
|
||||
} ) );
|
||||
}
|
||||
|
||||
return Promise.resolve( list );
|
||||
};
|
||||
function createTransformer(): ModelTransformer< SettingGroup > {
|
||||
return new ModelTransformer(
|
||||
[
|
||||
new KeyChangeTransformation< SettingGroup >( { parentID: 'parent_id' } ),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -28,8 +21,10 @@ function restList( httpClient: HTTPClient ): ListFn< SettingGroupRepositoryParam
|
|||
* @return {ListsSettingGroups} The created repository.
|
||||
*/
|
||||
export function settingGroupRESTRepository( httpClient: HTTPClient ): ListsSettingGroups {
|
||||
const transformer = createTransformer();
|
||||
|
||||
return new ModelRepository(
|
||||
restList( httpClient ),
|
||||
restList< SettingGroupRepositoryParams >( () => '/wc/v3/settings', SettingGroup, httpClient, transformer ),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
|
|
|
@ -1,10 +1,5 @@
|
|||
import { HTTPClient } from '../../../http';
|
||||
import {
|
||||
ListChildFn,
|
||||
ModelRepository,
|
||||
ReadChildFn,
|
||||
UpdateChildFn,
|
||||
} from '../../../framework/model-repository';
|
||||
import { ModelRepository, ParentID } from '../../../framework/model-repository';
|
||||
import { Setting } from '../../../models';
|
||||
import {
|
||||
ListsSettings,
|
||||
|
@ -12,61 +7,12 @@ import {
|
|||
SettingRepositoryParams,
|
||||
UpdatesSettings,
|
||||
} from '../../../models/settings/setting';
|
||||
import { ModelTransformer } from '../../../framework/model-transformer';
|
||||
import { restListChild, restReadChild, restUpdateChild } from '../shared';
|
||||
import { ModelID } from '../../../models/model';
|
||||
|
||||
function restList( httpClient: HTTPClient ): ListChildFn< SettingRepositoryParams > {
|
||||
return async ( parent ) => {
|
||||
const response = await httpClient.get( '/wc/v3/settings/' + parent );
|
||||
|
||||
const list: Setting[] = [];
|
||||
for ( const raw of response.data ) {
|
||||
list.push( new Setting( {
|
||||
id: raw.id,
|
||||
label: raw.label,
|
||||
description: raw.description,
|
||||
type: raw.type,
|
||||
options: raw.options,
|
||||
default: raw.default,
|
||||
value: raw.value,
|
||||
} ) );
|
||||
}
|
||||
|
||||
return Promise.resolve( list );
|
||||
};
|
||||
}
|
||||
|
||||
function restRead( httpClient: HTTPClient ): ReadChildFn< SettingRepositoryParams > {
|
||||
return async ( parent, id ) => {
|
||||
const response = await httpClient.get( '/wc/v3/settings/' + parent + '/' + id );
|
||||
|
||||
return Promise.resolve( new Setting( {
|
||||
id: response.data.id,
|
||||
label: response.data.label,
|
||||
description: response.data.description,
|
||||
type: response.data.type,
|
||||
options: response.data.options,
|
||||
default: response.data.default,
|
||||
value: response.data.value,
|
||||
} ) );
|
||||
};
|
||||
}
|
||||
|
||||
function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryParams > {
|
||||
return async ( parent, id, params ) => {
|
||||
const response = await httpClient.patch(
|
||||
'/wc/v3/settings/' + parent + '/' + id,
|
||||
params,
|
||||
);
|
||||
|
||||
return Promise.resolve( new Setting( {
|
||||
id: response.data.id,
|
||||
label: response.data.label,
|
||||
description: response.data.description,
|
||||
type: response.data.type,
|
||||
options: response.data.options,
|
||||
default: response.data.default,
|
||||
value: response.data.value,
|
||||
} ) );
|
||||
};
|
||||
function createTransformer(): ModelTransformer< Setting > {
|
||||
return new ModelTransformer( [] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -76,11 +22,14 @@ function restUpdate( httpClient: HTTPClient ): UpdateChildFn< SettingRepositoryP
|
|||
* @return {ListsSettings|ReadsSettings|UpdatesSettings} The created repository.
|
||||
*/
|
||||
export function settingRESTRepository( httpClient: HTTPClient ): ListsSettings & ReadsSettings & UpdatesSettings {
|
||||
const buildURL = ( parent: ParentID< SettingRepositoryParams >, id: ModelID ) => '/wc/v3/settings/' + parent + '/' + id;
|
||||
const transformer = createTransformer();
|
||||
|
||||
return new ModelRepository(
|
||||
restList( httpClient ),
|
||||
restListChild< SettingRepositoryParams >( ( parent ) => '/wc/v3/settings/' + parent, Setting, httpClient, transformer ),
|
||||
null,
|
||||
restRead( httpClient ),
|
||||
restUpdate( httpClient ),
|
||||
restReadChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
|
||||
restUpdateChild< SettingRepositoryParams >( buildURL, Setting, httpClient, transformer ),
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue